跨域不能携带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 {
@Autowired
private OperatorMapper operatorMapper;
@PostMapping("login")
public Result login(HttpServletResponse response, String username, String password){
//执行登录操作,签发jwt ,但不走shiro
log.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+"不存在或者密码不正确");
}
//响应里返回jwt
String jwt = JwtUtils.createJWT(user);
response.setHeader(JwtUtils.AUTH_TOKEN_NAME,jwt);
return Result.success(jwt);
}
}
2. 创建filter,验证jwt,filter调用realm
```java
package 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;
@Component
public class JWTFilter extends AuthenticatingFilter {
private static final Logger logger = LoggerFactory.getLogger(JWTFilter.class);
@Override
protected 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);
}
@Override
protected 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
*/
@Override
protected 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);
}
@Override
protected 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 {
// 返回401
httpServletResponse.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));
}
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
//TODO 刷新token
logger.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;
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
@Override
protected 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;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected 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
*/
@Bean
public Realm shiroRealm(){
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCacheManager(cacheManager());
return shiroRealm;
}
@Bean
public MemoryConstrainedCacheManager cacheManager(){
return new MemoryConstrainedCacheManager();
}
/**
* shiro的过滤器链
* @return
*/
@Bean
public 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 并取名为jwt
LinkedHashMap<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;
}
}
其他工具类:
```java
package 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;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public 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的body
JwtBuilder 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) {
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(SIGNING_KEY)
//设置需要解析的token
.parseClaimsJws(token).getBody();
return claims;
}
}
前端自动携带jwt,在main.js中配置axios
// axios.defaults.withCredentials=true
axios.defaults.baseURL="http://localhost:8081";
//添加拦截器,每次请求携带token
axios.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