1、概述
Servlet
的Session
机制流程如下
- 用户首次发请求
- 服务器接收到请求之后,无论你有没有权限访问到资源,在返回响应的时候,服务器都会生成一个
Session
用来储存该用户的信息,然后生成SessionId
作为对应的Key
- 服务器会在响应中,用
SessionId
这个名字,把这个SessionId
以Cookie
的方式发给客户(就是Set-Cookie
响应头) - 由于已经设置了
Cookie
,下次访问的时候,服务器会自动识别到这个SessionId
然后找到你上次对应的Session
引入
Shiro
后,新的流程如下
- 用户首次发请求。
- 服务器接收到请求之后,无论你有没有权限访问到资源,在返回响应的时候,服务器都会生成一个
Session
用来储存该用户的信息,然后生成SessionId
作为对应的Key
,还会创建一个Subject
对象(就是Shiro
中用来代表当前用户的类),也用这个SessionId
作为Key
绑定。 - 服务器会在响应中,用
SessionId
这个名字,把这个SessionId
以Cookie
的方式发给客户(就是Set-Cookie
响应头)。 - 第二次接受到请求的时候,
Shiro
会从请求头中找到SessionId
,然后去寻找对应的Subject
然后绑定到当前上下文,这时候Shiro
就能知道来访的是谁了。
对于以上流程,都和浏览器中的Cookie
密切相关的,对于一个前后端分离的系统而言,一般是需要支持多端的,一个api
要支持H5
, PC
和APP
三个前端,如果使用session
的话对app
不是很友好,而且session
有跨域攻击的问题
2、整合流程
Shiro
集成JWT
需要禁用session
,禁用后服务器将不会再维护用户的状态,达到无状态调用的目的
/**
* @param realm
* @return DefaultWebSecurityManager
* @description 注入安全管理器
* @author PengHuAnZhi
* @date 2021/11/4 20:50
*/
@Bean("defaultWebSecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(CustomRealm realm) {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
//开启全局缓存
realm.setCachingEnabled(true);
//开启认证缓存
realm.setAuthenticationCachingEnabled(true);
realm.setAuthenticationCacheName("authenticationCache");
//开启授权缓存
realm.setAuthorizationCachingEnabled(true);
realm.setAuthorizationCacheName("authorizationCache");
realm.setCacheManager(new EhCacheManager());
defaultSecurityManager.setRealm(realm);
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultSecurityManager.setSubjectDAO(subjectDAO);
return defaultSecurityManager;
}
/**
* @author PengHuAnZhi
* @ProjectName ShiroSpringBootDemo
* @Description TODO
* @time 2021/11/13 16:25
*/
public class NoSessionWebSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
// 禁用session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
然后定义一个
JwtToken
,用于封装UserName
和Token
,并且要使其充当Shiro中的令牌,所以需要实现AuthenticationToken
接口,重写里面的获取用户信息getPrincipal
和获取凭证信息的getCredentials
两个方法
/**
* @author PengHuAnZhi
* @ProjectName ShiroSpringBootDemo
* @Description TODO
* @time 2021/11/13 15:36
*/
public class JwtToken implements AuthenticationToken {
private final String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
定义
Jwt
相关的工具类
/**
* @author PengHuAnZhi
* @ProjectName ShiroSpringBootDemo
* @Description TODO
* @time 2021/11/13 15:49
*/
public class JwtUtil {
/**
* JWT-account
*/
public static final String ACCOUNT = "userName";
/**
* JWT-currentTimeMillis
*/
public final static String CURRENT_TIME_MILLIS = "currentTimeMillis";
/**
* 有效期时间2小时
*/
public static final long EXPIRE_TIME = 2 * 60 * 60 * 1000L;
/**
* 秘钥
*/
public static final String SECRET_KEY = "shiroKey";
/**
* 生成签名返回token
*
* @param userName 用户名
* @param currentTimeMillis 当前时间戳
* @return 返回一个token
*/
public static String sign(String userName, String currentTimeMillis) {
// 帐号加JWT私钥加密
String secret = userName + SECRET_KEY;
// 此处过期时间,单位:毫秒,在当前时间到后边的20分钟内都是有效的
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
//采用HMAC256加密
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create()
.withClaim(ACCOUNT, userName)
.withClaim(CURRENT_TIME_MILLIS, currentTimeMillis)
.withExpiresAt(date)
//创建一个新的JWT,并使用给定的算法进行标记
.sign(algorithm);
}
/**
* 校验token是否正确
*
* @param token token
* @return 是否登录成功
*/
public static boolean verify(String token) {
String secret = getClaim(token, ACCOUNT) + SECRET_KEY;
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.build();
verifier.verify(token);
return true;
}
/**
* 获得Token中的信息无需secret解密也能获得
*
* @param token token
* @param claim token中的信息
* @return 返回解密后的信息
*/
public static String getClaim(String token, String claim) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(claim).asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
然后再定义一个
JwtFilter
用于过滤所有请求,继承BasicHttpAuthenticationFilter
,将其交给Shiro
验证
/**
* @author PengHuAnZhi
* @ProjectName ShiroSpringBootDemo
* @Description TODO
* @time 2021/11/13 15:39
*/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter implements Filter {
/**
* 设置请求头中需要传递的字段名
*/
protected static final String AUTHORIZATION_HEADER = "Access-Token";
/**
* @param request 请求
* @param response 响应
* @param mappedValue 就是[urls]配置中拦截器参数部分
* @return boolean 表示是否允许访问,如果允许访问返回true,否则false
* @date 2020/11/24
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
JwtToken token = new JwtToken(((HttpServletRequest) request).getHeader(AUTHORIZATION_HEADER));
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* @param request 请求
* @param response 响应
* @return boolean 表示当访问拒绝时是否已经处理了,如果返回true表示需要继续处理,如果返回false表示该拦截器实例已经处理了,将直接返回即可
* @date 2020/11/24
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
String jsonWebToken = ((HttpServletRequest) request).getHeader(AUTHORIZATION_HEADER);
String username = "";
if (StringUtils.isBlank(jsonWebToken)) {
jsonWebToken = "";
} else {
// 解码 jwt
DecodedJWT decodeJwt = JWT.decode(jsonWebToken);
username = decodeJwt.getClaim("userName").asString();
System.out.println(username + "登录");
}
JwtToken token = new JwtToken(jsonWebToken);
try {
// 交给自定义realm进行jwt验证和对应角色,权限的查询
getSubject(request, response).login(token);
} catch (AuthenticationException e) {
request.setAttribute("msg", "认证失败");
// 转发给指定的 controller, 进行统一异常处理
request.getRequestDispatcher("/exception").forward(request, response);
return false;
}
return true;
}
}
在自定义
Realm
中验证jwt
,role
,permission
/**
* @author PengHuAnZhi
* @ProjectName ShiroSpringBootDemo
* @Description TODO
* @time 2021/11/4 20:44
*/
@Component
public class CustomRealm extends AuthorizingRealm {
@Resource
UserServiceImpl userServiceImpl;
/**
* @param token jwt
* @return boolean
* @description 验证是不是自己的token类型
* @author PengHuAnZhi
* @date 2021/11/13 16:06
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取用户名, 用户唯一标识
String username = JwtUtil.getClaim(principals.toString(), "userName");
User user = userServiceImpl.findByUserName(username);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
if (user == null) {
return null;
}
authorizationInfo.addRole("admin");
authorizationInfo.addStringPermission("user:update:*");
authorizationInfo.addStringPermission("product:*:*");
return authorizationInfo;
}
@SneakyThrows
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String credentials = (String) token.getCredentials();
String userName;
try {
//jwt验证token
boolean verify = JwtUtil.verify(credentials);
if (!verify) {
throw new AuthenticationException("Token校验不正确");
}
userName = JwtUtil.getClaim(credentials, JwtUtil.ACCOUNT);
} catch (Exception e) {
throw new Exception("用户身份校验失败");
}
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,不设置则使用默认的SimpleCredentialsMatcher
return new SimpleAuthenticationInfo(
//用户名
userName,
//凭证
credentials,
//realm name
this.getName());
}
}
定义一个登录接口
@RequestMapping("/login")
public String login(String userName, String password, HttpServletResponse response) {
try {
if (!"phz".equals(userName) || !"123".equals(password)) {
System.out.println("用户名错误");
return "redirect:/login.jsp";
}
//生成token
String token = JwtUtil.sign(userName, System.currentTimeMillis());
//写入header
response.setHeader("Access-Token", token);
response.setHeader("Access-Control-Expose-Headers", "Access-Token");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
return "redirect:/login.jsp";
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
return "redirect:/login.jsp";
}
return "redirect:/index.jsp";
}
最后将自己定义的
Filter
添加到Shiro
中
/**
* @return ShiroFilterFactoryBean
* @description 1、创建ShiroFilter,拦截所有请求
* @author PengHuAnZhi
* @date 2021/11/4 20:43
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加自定义过滤器
Map<String, Filter> filterMap = new HashMap<>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//配置系统受限资源和公共资源
Map<String, String> map = new HashMap<>();
map.put("/register.jsp", ANON);
map.put("/login.jsp", ANON);
map.put("/user/login", ANON);
map.put("/user/register", ANON);
//使用自己的filter对所有请求拦截
map.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//不设置默认也是login.jsp
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
return shiroFilterFactoryBean;
}