跨域不能携带cookie => jsessionId不能传递 => session失效
shiro: 认证、授权、会话管理、加密、缓存…
认证状态存在session中,若session失效,则shiro无法判断是否登录
解决方案:
- 降低浏览器版本,让跨域时能够携带cookie
- 使用无状态登录技术(jwt)

Shiro改造

思路:
- 登录请求不走shiro,自行实现,并在成功后颁发jwt
- 所有的请求都走shiro自定义过滤器
- 自定义过滤器内部获取jwt,并根据jwt中的用户信息获取权限信息并判断是否有权限
修改方式:
- 登录请求处理
认证逻辑
登录时不走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 {
@Autowiredprivate OperatorMapper operatorMapper;@PostMapping("login")public Result login(HttpServletResponse response, String username, String password){//执行登录操作,签发jwt ,但不走shirolog.info("登录操作:{},{}",username,password);OperatorExample example = new OperatorExample();example.createCriteria().andOperatorNameEqualTo(username).andPasswordEqualTo(password);List<Operator> operators = operatorMapper.selectByExample(example);Operator user = operators.get(0);if(user==null||operators.size()>1){return Result.fail("用户"+username+"不存在或者密码不正确");}//响应里返回jwtString jwt = JwtUtils.createJWT(user);response.setHeader(JwtUtils.AUTH_TOKEN_NAME,jwt);return Result.success(jwt);}
}
2. 创建filter,验证jwt,filter调用realm```javapackage me.maiz.project.eduk15boss.components;import com.fasterxml.jackson.databind.ObjectMapper;import com.google.common.base.Strings;import io.jsonwebtoken.Claims;import me.maiz.project.eduk15boss.common.JwtUtils;import me.maiz.project.eduk15boss.common.Result;import me.maiz.project.eduk15boss.model.Operator;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authc.UnknownAccountException;import org.apache.shiro.subject.Subject;import org.apache.shiro.web.filter.authc.AuthenticatingFilter;import org.apache.shiro.web.util.WebUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.http.HttpStatus;import org.springframework.stereotype.Component;import org.springframework.web.bind.annotation.RequestMethod;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@Componentpublic class JWTFilter extends AuthenticatingFilter {private static final Logger logger = LoggerFactory.getLogger(JWTFilter.class);@Overrideprotected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {//全局允许跨域HttpServletRequest httpServletRequest = (HttpServletRequest) request;String requestURI = httpServletRequest.getRequestURI();logger.info("进入预处理:{}-{}",httpServletRequest.getMethod(),requestURI);HttpServletResponse httpServletResponse = (HttpServletResponse) response;httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));// 跨域时会首先发送一个 option请求,这里我们给 option请求直接返回正常状态if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {httpServletResponse.setStatus(HttpStatus.OK.value());return false;}return super.preHandle(request, response);}@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {logger.info("进入isAccessAllowed");if(!isJwtValid(request)){return false;}return super.isAccessAllowed(request, response, mappedValue);}private boolean isJwtValid(ServletRequest req){HttpServletRequest request = (HttpServletRequest) req;String token = request.getHeader(JwtUtils.AUTH_TOKEN_NAME);if(Strings.isNullOrEmpty(token)){logger.info("JWT未提供");return false;}try {Operator operator = JwtUtils.parseJwt(token);return operator!=null;}catch (Exception e){logger.info("JWT过期或者其他错误,{},{}",e.getMessage());request.setAttribute("AUTHC_FAIL","AUTHC_FAIL");return false;}}/*** 从请求中提取Token* @param req* @param response* @return* @throws Exception*/@Overrideprotected AuthenticationToken createToken(ServletRequest req, ServletResponse response) throws Exception {logger.info("进入createToken");HttpServletRequest request = (HttpServletRequest) req;String token = request.getHeader(JwtUtils.AUTH_TOKEN_NAME);if(Strings.isNullOrEmpty(token)){throw new UnknownAccountException("请求不合法,JWT token未传入");}return new JwtToken(token);}@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {logger.info("进入onAccessDenied");HttpServletRequest httpServletRequest = WebUtils.toHttp(request);HttpServletResponse httpServletResponse = WebUtils.toHttp(response);String token = httpServletRequest.getHeader(JwtUtils.AUTH_TOKEN_NAME);if(Strings.isNullOrEmpty(token)){fail(httpServletRequest, httpServletResponse);return false;}boolean executeLogin = executeLogin(httpServletRequest, httpServletResponse);if(!executeLogin){fail(httpServletRequest, httpServletResponse);}return executeLogin;}private void fail(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {// 返回401httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);// 设置响应码为401或者直接输出消息String url = httpServletRequest.getRequestURI();ObjectMapper objectMapper = new ObjectMapper();boolean isAuthc =httpServletRequest.getAttribute("AUTHC_FAIL")!=null;Result fail = Result.fail(isAuthc?"您未登录,请先登录":"未授权的请求,请先登录或取得授权");httpServletResponse.setContentType("application/json;charset=utf-8");httpServletResponse.getWriter().print(objectMapper.writeValueAsString(fail));}@Overrideprotected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {//TODO 刷新tokenlogger.info("进入onLoginSuccess");return super.onLoginSuccess(token, subject, request, response);}}
package me.maiz.project.eduk15boss.components;import com.google.common.collect.Sets;import lombok.extern.slf4j.Slf4j;import me.maiz.project.eduk15boss.common.JwtUtils;import me.maiz.project.eduk15boss.dao.OperatorMapper;import me.maiz.project.eduk15boss.model.Operator;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.AuthenticationInfo;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authc.SimpleAuthenticationInfo;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.authz.SimpleAuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;@Slf4jpublic class ShiroRealm extends AuthorizingRealm {@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {log.info("获取授权信息");Object primaryPrincipal = principals.getPrimaryPrincipal();Operator user = (Operator) primaryPrincipal;//TODO 查询权限等SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();simpleAuthorizationInfo.setRoles(null);simpleAuthorizationInfo.setStringPermissions(Sets.newHashSet("student:add"));return simpleAuthorizationInfo;}@Overridepublic boolean supports(AuthenticationToken token) {return token instanceof JwtToken;}@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {log.info("登录操作:{}",token);JwtToken jwtToken = (JwtToken) token;Operator operator = JwtUtils.parseJwt(jwtToken.getToken());return new SimpleAuthenticationInfo(operator, jwtToken.getToken().toCharArray(),getName());}}
- 配置过滤器,使其生效 ```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 {
/*** 自定义Realm 提供数据源* @return*/@Beanpublic Realm shiroRealm(){ShiroRealm shiroRealm = new ShiroRealm();shiroRealm.setCacheManager(cacheManager());return shiroRealm;}@Beanpublic MemoryConstrainedCacheManager cacheManager(){return new MemoryConstrainedCacheManager();}/*** shiro的过滤器链* @return*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);DefaultShiroFilterChainDefinition sfcd = new DefaultShiroFilterChainDefinition();ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
// filterFactoryBean.setLoginUrl(loginUrl); // filterFactoryBean.setSuccessUrl(successUrl); // filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
filterFactoryBean.setSecurityManager(securityManager);// 在 Shiro过滤器链上加入 自定义过滤器JWTFilter 并取名为jwtLinkedHashMap<String, Filter> filters = new LinkedHashMap<>();filters.put("jwt", new JWTFilter());shiroFilterFactoryBean.setFilters(filters);//放行 使用sfcd.addPathDefinition("/","anon");sfcd.addPathDefinition("/login","anon");sfcd.addPathDefinition("/error/404.html","anon");sfcd.addPathDefinition("/css/**","anon");sfcd.addPathDefinition("/js/**","anon");sfcd.addPathDefinition("/images/**","anon");sfcd.addPathDefinition("/fonts/**","anon");sfcd.addPathDefinition("/html/**","anon");//logout 登出sfcd.addPathDefinition("/logout","logout");//其他则需要认证sfcd.addPathDefinition("/**","jwt");shiroFilterFactoryBean.setFilterChainDefinitionMap(sfcd.getFilterChainMap());return shiroFilterFactoryBean;}
}
其他工具类:```javapackage me.maiz.project.eduk15boss.components;import lombok.Data;import lombok.experimental.Accessors;import org.apache.shiro.authc.AuthenticationToken;@Data@Accessors(chain = true)public class JwtToken implements AuthenticationToken {/*** 登录token*/private String token;public JwtToken(String token) {this.token = token;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic Object getCredentials() {return token;}}
package me.maiz.project.eduk15boss.common;import io.jsonwebtoken.Claims;import io.jsonwebtoken.JwtBuilder;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import me.maiz.project.eduk15boss.model.Operator;import java.util.Date;import java.util.HashMap;import java.util.Map;public class JwtUtils {public static final String AUTH_TOKEN_NAME="auth_token";/*** 签名用的密钥*/private static final String SIGNING_KEY = "78sebr72umyz33i9876gc31urjgyfhgj";private static final long DEFAULT_EXPIRATION_MILLIS=2*24*60*60*1000L;private static final String USER_ID_KEY = "userId";public static final String USERNAME_KEY = "username";public static String createJWT(Operator user) {Map<String,Object> claims = new HashMap<>();claims.put(USER_ID_KEY,user.getOperatorId());claims.put(USERNAME_KEY,user.getOperatorName());return create(DEFAULT_EXPIRATION_MILLIS,claims);}public static Operator parseJwt(String token){Claims claims = parse(token);Operator user = new Operator();user.setOperatorId((Integer) claims.get(USER_ID_KEY));user.setOperatorName((String) claims.get(USERNAME_KEY));return user;}/*** 用户登录成功后生成Jwt* 使用Hs256算法** @param expiredInMillis jwt过期时间* @param claims 保存在Payload(有效载荷)中的内容* @return token字符串*/public static String create(long expiredInMillis, Map<String, Object> claims) {//指定签名的时候使用的签名算法SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;//生成JWT的时间long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);//创建一个JwtBuilder,设置jwt的bodyJwtBuilder builder = Jwts.builder()//保存在Payload(有效载荷)中的内容.setClaims(claims)//iat: jwt的签发时间.setIssuedAt(now)//设置过期时间.setExpiration(new Date(nowMillis+expiredInMillis))//设置签名使用的签名算法和签名使用的秘钥.signWith(signatureAlgorithm, SIGNING_KEY);return builder.compact();}/*** 解析token,获取到Payload(有效载荷)中的内容,包括验证签名,判断是否过期** @param token* @return*/public static Claims parse(String token) {//得到DefaultJwtParserClaims claims = Jwts.parser()//设置签名的秘钥.setSigningKey(SIGNING_KEY)//设置需要解析的token.parseClaimsJws(token).getBody();return claims;}}
前端自动携带jwt,在main.js中配置axios
// axios.defaults.withCredentials=trueaxios.defaults.baseURL="http://localhost:8081";//添加拦截器,每次请求携带tokenaxios.interceptors.request.use(function (config) {const token = localStorage.getItem("jwt");config.headers.auth_token = token;return config;});
相关依赖:
<!--jwt--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!--guava工具类--><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>29.0-jre</version></dependency>
代码参见:
https://github.com/stickgoal/eduk15/tree/main/eduk15-boss

