跨域不能携带cookie => jsessionId不能传递 => session失效

shiro: 认证、授权、会话管理、加密、缓存…
认证状态存在session中,若session失效,则shiro无法判断是否登录

解决方案:

  • 降低浏览器版本,让跨域时能够携带cookie
  • 使用无状态登录技术(jwt)

image.png

https://jwt.io/
image.png

Shiro改造

image.png
思路:

  1. 登录请求不走shiro,自行实现,并在成功后颁发jwt
  2. 所有的请求都走shiro自定义过滤器
  3. 自定义过滤器内部获取jwt,并根据jwt中的用户信息获取权限信息并判断是否有权限

修改方式:

  1. 登录请求处理
  2. 认证逻辑

  3. 登录时不走shiro,直接走controller,成功后颁发jwt ```java package me.maiz.project.eduk15boss.controller;

import lombok.extern.slf4j.Slf4j; import me.maiz.project.eduk15boss.common.JwtUtils; import me.maiz.project.eduk15boss.common.Result; import me.maiz.project.eduk15boss.dao.OperatorMapper; import me.maiz.project.eduk15boss.model.Operator; import me.maiz.project.eduk15boss.model.OperatorExample; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse; import java.util.List;

@RestController @Slf4j public class LoginController {

  1. @Autowired
  2. private OperatorMapper operatorMapper;
  3. @PostMapping("login")
  4. public Result login(HttpServletResponse response, String username, String password){
  5. //执行登录操作,签发jwt ,但不走shiro
  6. log.info("登录操作:{},{}",username,password);
  7. OperatorExample example = new OperatorExample();
  8. example.createCriteria()
  9. .andOperatorNameEqualTo(username)
  10. .andPasswordEqualTo(password);
  11. List<Operator> operators = operatorMapper.selectByExample(example);
  12. Operator user = operators.get(0);
  13. if(user==null||operators.size()>1){
  14. return Result.fail("用户"+username+"不存在或者密码不正确");
  15. }
  16. //响应里返回jwt
  17. String jwt = JwtUtils.createJWT(user);
  18. response.setHeader(JwtUtils.AUTH_TOKEN_NAME,jwt);
  19. return Result.success(jwt);
  20. }

}

  1. 2. 创建filter,验证jwtfilter调用realm
  2. ```java
  3. package me.maiz.project.eduk15boss.components;
  4. import com.fasterxml.jackson.databind.ObjectMapper;
  5. import com.google.common.base.Strings;
  6. import io.jsonwebtoken.Claims;
  7. import me.maiz.project.eduk15boss.common.JwtUtils;
  8. import me.maiz.project.eduk15boss.common.Result;
  9. import me.maiz.project.eduk15boss.model.Operator;
  10. import org.apache.shiro.authc.AuthenticationToken;
  11. import org.apache.shiro.authc.UnknownAccountException;
  12. import org.apache.shiro.subject.Subject;
  13. import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
  14. import org.apache.shiro.web.util.WebUtils;
  15. import org.slf4j.Logger;
  16. import org.slf4j.LoggerFactory;
  17. import org.springframework.http.HttpStatus;
  18. import org.springframework.stereotype.Component;
  19. import org.springframework.web.bind.annotation.RequestMethod;
  20. import javax.servlet.ServletRequest;
  21. import javax.servlet.ServletResponse;
  22. import javax.servlet.http.HttpServletRequest;
  23. import javax.servlet.http.HttpServletResponse;
  24. import java.io.IOException;
  25. @Component
  26. public class JWTFilter extends AuthenticatingFilter {
  27. private static final Logger logger = LoggerFactory.getLogger(JWTFilter.class);
  28. @Override
  29. protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
  30. //全局允许跨域
  31. HttpServletRequest httpServletRequest = (HttpServletRequest) request;
  32. String requestURI = httpServletRequest.getRequestURI();
  33. logger.info("进入预处理:{}-{}",httpServletRequest.getMethod(),requestURI);
  34. HttpServletResponse httpServletResponse = (HttpServletResponse) response;
  35. httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
  36. httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
  37. httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
  38. // 跨域时会首先发送一个 option请求,这里我们给 option请求直接返回正常状态
  39. if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
  40. httpServletResponse.setStatus(HttpStatus.OK.value());
  41. return false;
  42. }
  43. return super.preHandle(request, response);
  44. }
  45. @Override
  46. protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
  47. logger.info("进入isAccessAllowed");
  48. if(!isJwtValid(request)){
  49. return false;
  50. }
  51. return super.isAccessAllowed(request, response, mappedValue);
  52. }
  53. private boolean isJwtValid(ServletRequest req){
  54. HttpServletRequest request = (HttpServletRequest) req;
  55. String token = request.getHeader(JwtUtils.AUTH_TOKEN_NAME);
  56. if(Strings.isNullOrEmpty(token)){
  57. logger.info("JWT未提供");
  58. return false;
  59. }
  60. try {
  61. Operator operator = JwtUtils.parseJwt(token);
  62. return operator!=null;
  63. }catch (Exception e){
  64. logger.info("JWT过期或者其他错误,{},{}",e.getMessage());
  65. request.setAttribute("AUTHC_FAIL","AUTHC_FAIL");
  66. return false;
  67. }
  68. }
  69. /**
  70. * 从请求中提取Token
  71. * @param req
  72. * @param response
  73. * @return
  74. * @throws Exception
  75. */
  76. @Override
  77. protected AuthenticationToken createToken(ServletRequest req, ServletResponse response) throws Exception {
  78. logger.info("进入createToken");
  79. HttpServletRequest request = (HttpServletRequest) req;
  80. String token = request.getHeader(JwtUtils.AUTH_TOKEN_NAME);
  81. if(Strings.isNullOrEmpty(token)){
  82. throw new UnknownAccountException("请求不合法,JWT token未传入");
  83. }
  84. return new JwtToken(token);
  85. }
  86. @Override
  87. protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
  88. logger.info("进入onAccessDenied");
  89. HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
  90. HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
  91. String token = httpServletRequest.getHeader(JwtUtils.AUTH_TOKEN_NAME);
  92. if(Strings.isNullOrEmpty(token)){
  93. fail(httpServletRequest, httpServletResponse);
  94. return false;
  95. }
  96. boolean executeLogin = executeLogin(httpServletRequest, httpServletResponse);
  97. if(!executeLogin){
  98. fail(httpServletRequest, httpServletResponse);
  99. }
  100. return executeLogin;
  101. }
  102. private void fail(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {
  103. // 返回401
  104. httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
  105. // 设置响应码为401或者直接输出消息
  106. String url = httpServletRequest.getRequestURI();
  107. ObjectMapper objectMapper = new ObjectMapper();
  108. boolean isAuthc =httpServletRequest.getAttribute("AUTHC_FAIL")!=null;
  109. Result fail = Result.fail(isAuthc?"您未登录,请先登录":"未授权的请求,请先登录或取得授权");
  110. httpServletResponse.setContentType("application/json;charset=utf-8");
  111. httpServletResponse.getWriter().print(objectMapper.writeValueAsString(fail));
  112. }
  113. @Override
  114. protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
  115. //TODO 刷新token
  116. logger.info("进入onLoginSuccess");
  117. return super.onLoginSuccess(token, subject, request, response);
  118. }
  119. }
  1. package me.maiz.project.eduk15boss.components;
  2. import com.google.common.collect.Sets;
  3. import lombok.extern.slf4j.Slf4j;
  4. import me.maiz.project.eduk15boss.common.JwtUtils;
  5. import me.maiz.project.eduk15boss.dao.OperatorMapper;
  6. import me.maiz.project.eduk15boss.model.Operator;
  7. import org.apache.shiro.authc.AuthenticationException;
  8. import org.apache.shiro.authc.AuthenticationInfo;
  9. import org.apache.shiro.authc.AuthenticationToken;
  10. import org.apache.shiro.authc.SimpleAuthenticationInfo;
  11. import org.apache.shiro.authz.AuthorizationInfo;
  12. import org.apache.shiro.authz.SimpleAuthorizationInfo;
  13. import org.apache.shiro.realm.AuthorizingRealm;
  14. import org.apache.shiro.subject.PrincipalCollection;
  15. import org.springframework.beans.factory.annotation.Autowired;
  16. @Slf4j
  17. public class ShiroRealm extends AuthorizingRealm {
  18. @Override
  19. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  20. log.info("获取授权信息");
  21. Object primaryPrincipal = principals.getPrimaryPrincipal();
  22. Operator user = (Operator) primaryPrincipal;
  23. //TODO 查询权限等
  24. SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
  25. simpleAuthorizationInfo.setRoles(null);
  26. simpleAuthorizationInfo.setStringPermissions(Sets.newHashSet("student:add"));
  27. return simpleAuthorizationInfo;
  28. }
  29. @Override
  30. public boolean supports(AuthenticationToken token) {
  31. return token instanceof JwtToken;
  32. }
  33. @Override
  34. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  35. log.info("登录操作:{}",token);
  36. JwtToken jwtToken = (JwtToken) token;
  37. Operator operator = JwtUtils.parseJwt(jwtToken.getToken());
  38. return new SimpleAuthenticationInfo(operator, jwtToken.getToken().toCharArray(),getName());
  39. }
  40. }
  1. 配置过滤器,使其生效 ```java package me.maiz.project.eduk15boss.config;

import me.maiz.project.eduk15boss.components.JWTFilter; import me.maiz.project.eduk15boss.components.ShiroRealm; import org.apache.shiro.cache.MemoryConstrainedCacheManager; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition; import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter; import java.util.LinkedHashMap;

@Configuration public class ShiroConfig {

  1. /**
  2. * 自定义Realm 提供数据源
  3. * @return
  4. */
  5. @Bean
  6. public Realm shiroRealm(){
  7. ShiroRealm shiroRealm = new ShiroRealm();
  8. shiroRealm.setCacheManager(cacheManager());
  9. return shiroRealm;
  10. }
  11. @Bean
  12. public MemoryConstrainedCacheManager cacheManager(){
  13. return new MemoryConstrainedCacheManager();
  14. }
  15. /**
  16. * shiro的过滤器链
  17. * @return
  18. */
  19. @Bean
  20. public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
  21. ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  22. shiroFilterFactoryBean.setSecurityManager(securityManager);
  23. DefaultShiroFilterChainDefinition sfcd = new DefaultShiroFilterChainDefinition();
  24. ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();

// filterFactoryBean.setLoginUrl(loginUrl); // filterFactoryBean.setSuccessUrl(successUrl); // filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);

  1. filterFactoryBean.setSecurityManager(securityManager);
  2. // 在 Shiro过滤器链上加入 自定义过滤器JWTFilter 并取名为jwt
  3. LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
  4. filters.put("jwt", new JWTFilter());
  5. shiroFilterFactoryBean.setFilters(filters);
  6. //放行 使用
  7. sfcd.addPathDefinition("/","anon");
  8. sfcd.addPathDefinition("/login","anon");
  9. sfcd.addPathDefinition("/error/404.html","anon");
  10. sfcd.addPathDefinition("/css/**","anon");
  11. sfcd.addPathDefinition("/js/**","anon");
  12. sfcd.addPathDefinition("/images/**","anon");
  13. sfcd.addPathDefinition("/fonts/**","anon");
  14. sfcd.addPathDefinition("/html/**","anon");
  15. //logout 登出
  16. sfcd.addPathDefinition("/logout","logout");
  17. //其他则需要认证
  18. sfcd.addPathDefinition("/**","jwt");
  19. shiroFilterFactoryBean.setFilterChainDefinitionMap(sfcd.getFilterChainMap());
  20. return shiroFilterFactoryBean;
  21. }

}

  1. 其他工具类:
  2. ```java
  3. package me.maiz.project.eduk15boss.components;
  4. import lombok.Data;
  5. import lombok.experimental.Accessors;
  6. import org.apache.shiro.authc.AuthenticationToken;
  7. @Data
  8. @Accessors(chain = true)
  9. public class JwtToken implements AuthenticationToken {
  10. /**
  11. * 登录token
  12. */
  13. private String token;
  14. public JwtToken(String token) {
  15. this.token = token;
  16. }
  17. @Override
  18. public Object getPrincipal() {
  19. return token;
  20. }
  21. @Override
  22. public Object getCredentials() {
  23. return token;
  24. }
  25. }
  1. package me.maiz.project.eduk15boss.common;
  2. import io.jsonwebtoken.Claims;
  3. import io.jsonwebtoken.JwtBuilder;
  4. import io.jsonwebtoken.Jwts;
  5. import io.jsonwebtoken.SignatureAlgorithm;
  6. import me.maiz.project.eduk15boss.model.Operator;
  7. import java.util.Date;
  8. import java.util.HashMap;
  9. import java.util.Map;
  10. public class JwtUtils {
  11. public static final String AUTH_TOKEN_NAME="auth_token";
  12. /**
  13. * 签名用的密钥
  14. */
  15. private static final String SIGNING_KEY = "78sebr72umyz33i9876gc31urjgyfhgj";
  16. private static final long DEFAULT_EXPIRATION_MILLIS=2*24*60*60*1000L;
  17. private static final String USER_ID_KEY = "userId";
  18. public static final String USERNAME_KEY = "username";
  19. public static String createJWT(Operator user) {
  20. Map<String,Object> claims = new HashMap<>();
  21. claims.put(USER_ID_KEY,user.getOperatorId());
  22. claims.put(USERNAME_KEY,user.getOperatorName());
  23. return create(DEFAULT_EXPIRATION_MILLIS,claims);
  24. }
  25. public static Operator parseJwt(String token){
  26. Claims claims = parse(token);
  27. Operator user = new Operator();
  28. user.setOperatorId((Integer) claims.get(USER_ID_KEY));
  29. user.setOperatorName((String) claims.get(USERNAME_KEY));
  30. return user;
  31. }
  32. /**
  33. * 用户登录成功后生成Jwt
  34. * 使用Hs256算法
  35. *
  36. * @param expiredInMillis jwt过期时间
  37. * @param claims 保存在Payload(有效载荷)中的内容
  38. * @return token字符串
  39. */
  40. public static String create(long expiredInMillis, Map<String, Object> claims) {
  41. //指定签名的时候使用的签名算法
  42. SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
  43. //生成JWT的时间
  44. long nowMillis = System.currentTimeMillis();
  45. Date now = new Date(nowMillis);
  46. //创建一个JwtBuilder,设置jwt的body
  47. JwtBuilder builder = Jwts.builder()
  48. //保存在Payload(有效载荷)中的内容
  49. .setClaims(claims)
  50. //iat: jwt的签发时间
  51. .setIssuedAt(now)
  52. //设置过期时间
  53. .setExpiration(new Date(nowMillis+expiredInMillis))
  54. //设置签名使用的签名算法和签名使用的秘钥
  55. .signWith(signatureAlgorithm, SIGNING_KEY);
  56. return builder.compact();
  57. }
  58. /**
  59. * 解析token,获取到Payload(有效载荷)中的内容,包括验证签名,判断是否过期
  60. *
  61. * @param token
  62. * @return
  63. */
  64. public static Claims parse(String token) {
  65. //得到DefaultJwtParser
  66. Claims claims = Jwts.parser()
  67. //设置签名的秘钥
  68. .setSigningKey(SIGNING_KEY)
  69. //设置需要解析的token
  70. .parseClaimsJws(token).getBody();
  71. return claims;
  72. }
  73. }

前端自动携带jwt,在main.js中配置axios

  1. // axios.defaults.withCredentials=true
  2. axios.defaults.baseURL="http://localhost:8081";
  3. //添加拦截器,每次请求携带token
  4. axios.interceptors.request.use(function (config) {
  5. const token = localStorage.getItem("jwt");
  6. config.headers.auth_token = token;
  7. return config;
  8. });

相关依赖:

  1. <!--jwt-->
  2. <dependency>
  3. <groupId>io.jsonwebtoken</groupId>
  4. <artifactId>jjwt</artifactId>
  5. <version>0.9.1</version>
  6. </dependency>
  7. <!--guava工具类-->
  8. <dependency>
  9. <groupId>com.google.guava</groupId>
  10. <artifactId>guava</artifactId>
  11. <version>29.0-jre</version>
  12. </dependency>

代码参见:
https://github.com/stickgoal/eduk15/tree/main/eduk15-boss