引入shiro-redis包依赖+jwt 配置流程及代码实现
路线
导入shiro-redis 依赖之后,就需要根据官方指示去实现两个bean的定义
1、在shiroConifg中定义这两个方法SessionManager
and SessionsSecurityManager
2、在注入这两个注解redisSessionDAO
and redisCacheManager
,并重写上面两个方法
3、我们后端接收的请求都需要走jwtFilter,所以需要在shiroConifg中定义两个跟jwtFilter有关的方法,ShiroFilterChainDefinition
和ShiroFilterFactory
4、在shiroFilterFactoryBean
方法中需要一个jwtFilter,让所有请求进来前都走jwtFilter这个方法
5、创建JwtFilter并实现AuthenticatingFilter
接口,后端接受请求时都会先走jwtFilter,从请求中获取请求头,如果请求头中带有Authorization : token 这样一个kv键值对,即收到的这个请求时带有token的,就封装成JwtToken并返回,也就是new JwtToken(token)
,注意:传入的参数token是我们从前端发出请求的请求头中获取的,获取方式是 (HttpservletRequest)servletRequest.getHeader("Authorizaton")
6、上一步说到我们如果从前端请求拿到的token不为空,该方法就返回一个JwtToken类,所以我们需要创建一个JwtToken的工具类,专门用来封装传入的token
7、在jwtFilter的createToken方法返回封装好的jwtToken,会在我们自定义jwtFilter继承的AuthenticatingFilter
类中的executeLogin方法里面去调用我们jwtFilter的createdToken方法,并拿到我们返回的jwtToken,在subject.login(token),这个token是被转换过类型的AuthenticationToken token = this.createToken(request, response);
8、上一步说到在AuthenticatingFilter
类中的executeLogin方法里进行认证subject.login(token),最终会走Realm里面进行认证,在shiroConifg里面的**securityManger()**方法里面也需要传入Realm,所以我们要自定义个Realm,并继承AuthorizingRealm
类
9、我们自定义一个Realm去继承AuthorizingRealm
,并重写supports方法,让其返回的token是jwtToken
10、我们回到JwtFilter,在进行登录后对请求进行的一个拦截,走onAccessDenied()
方法判断jwt是否过期失效,如果token为空的时候,我们不需要交给shiro进行登录处理,直接返回true,不需要在进行拦截,让其去访问没有添加认证注解的公共类。如果jwt不为空,就走shiro登录处理,使用jwtUtils判断jwt时效性
11、我们导入了jjwt依赖,但是还不够满足我们的需求,所以需要自定义一个jwtUtils,通过application.yml对该工具类的一些属性进行赋值,该工具类可以生成token,getClaimByToken(token)
进行一个校验,isTokenExpired判断是否过期。
12、回到jwtFilter的onAccessDenied()
继续判断token是否有效,判断正常就会进行一个登录处理,执行executeLogin()
方法, 在AuthenticationFilter
里的executeLogin(),会交给subject.login(token)
,最终走的是自定义的Realm的doGetAuthentiactionInfo
进行认证处理
13、上一步我们说到最终会走到自定义Realm的doGetAuthenticationInfo
进行认证,而认证不通过也就是通过token拿到的用户信息不存在或者已被锁定(某些操作权限被禁止),就会抛出异常,这个异常会在AuthenticatingFilter
的executeLogin()方法进行一个捕获,返回一个onLoginFailure()
的异常方法,而我们做的是前后端分离,并且我们返回前端的数据是有一定的格式,也就是封装成Result类,所以当出现这样一个异常的时候,我们最好就对这个onLoginFailure()
方法进行一个重写,让其返回我们想要看到的结果
14、接下来是shiro的登录逻辑,前面我们讲到后端接受到请求会被ShiroFilter里面shiroFilterFactoryBean()
方法定义的jwtFilter所拦截,如果是初次登陆,就会根据用户名密码走到jwtFilter里面createdToekn()放在,最终走到AuthenticatingFiler
里面的createdToken,实际上也是调用UsernamePasswordToken
,这跟我们之前使用原生的jwt生成方法一样。如果不是初次登陆,就会判断token的有效性最后走到我们自定义的Realm进行一个认证授权处理
15、我们shiro的登录逻辑开发实际上就是对Realm进行一个代码编写,因为shiro三大核心就是subject,SecurityMessage,还有Realm,所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager,所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject,SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法
16、自定义Realm,为了解析我们的jwtToken,我们需要注入JwtUtils,使用jwtUtils.getClaimByToken()
去获取jwtToken的身份信息,jwtToken.getPrincipal().getSubject()
方法是可以直接拿到用户id,在通过id查询出用户信息,在对拿到的用户信息进行非空判断,在第13话我们提到会在这个方法抛出异常。通过非空判断后需要返回SimpleAuthenticationInfo()
,其中是三个参数分别为用户的基本信息,密钥信息,用户名字,而用户的基本信息是不包括用户的敏感信息的,但是我们通过id查询到的用户信息是完整的,这里我们定义了一个AccountProfile
这样的一个VO实体类,来保存用户的基本信息,同样这个类也需要实现序列化。我们使用springframework.BeanUtil.copyProperties
进行实体类复制,AccountProfile只会拿到自己拥有的属性
考虑到后面可能需要做集群、负载均衡等,所以就需要会话共享,而shiro的缓存和会话信息,我们一般考虑使用redis来存储这些数据,所以,我们不仅仅需要整合shiro,同时也需要整合redis。在开源的项目中,我们找到了一个starter可以快速整合shiro-redis,配置简单,这里也推荐大家使用。
而因为我们需要做的是前后端分离项目的骨架,所以一般我们会采用token或者jwt作为跨域身份验证解决方案。所以整合shiro的过程中,我们需要引入jwt的身份验证过程。
1、实现流程图
2、定义配置类
1、Shiro-redis的引入
导入依赖
<!--shiro-redis-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!--hutool工具类 处理业务,简化代码-->
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.3</version>
</dependency>
<!--jwt-->
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、重写官方指定的方法
根据官方文档shiro-redis介绍
https://github.com/alexxiyang/shiro-redis/blob/master/docs/README.md#spring-boot-starter
我们需要导入两个实现方法
The next step depends on whether you’ve created your own SessionManager
or SessionsSecurityManager
.
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// inject redisSessionDAO
sessionManager.setSessionDAO(redisSessionDAO);
// other stuff...
return sessionManager;
}
@Bean
public SessionsSecurityManager securityManager(List<Realm> realms, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realms);
//inject sessionManager
securityManager.setSessionManager(sessionManager);
// inject redisCacheManager
securityManager.setCacheManager(redisCacheManager);
// other stuff...
return securityManager;
}
Then inject redisSessionDAO
and redisCacheManager
which created by shiro-redis-spring-boot-starter
already
如果我们已经导入了shiro-redis-spring-boot-starter
,就需要添加以下两个注解
@Autowired
RedisSessionDAO redisSessionDAO;
@Autowired
RedisCacheManager redisCacheManager;
注意:**public SessionsSecurityManager securityManager(List realms, SessionManager sessionManager)**方法中的Realm我们需要自己定义,所以必须重写这个方法
@Bean
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// inject redisSessionDAO
sessionManager.setSessionDAO(redisSessionDAO);
// other stuff...
return sessionManager;
}
@Bean
public SessionsSecurityManager securityManager(AccountRealm accountRealm,
SessionManager sessionManager,
RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
//inject sessionManager
securityManager.setSessionManager(sessionManager);
// inject redisCacheManager
securityManager.setCacheManager(redisCacheManager);
// other stuff...
return securityManager;
}
3、ShiroConif中添加ShiroFilterChainDefinition和ShiroFilterFactory
在流程图中有一个JwtFilter过滤器,判断用户有无jwt,是否放行访问
我们在这里使用这个过滤器
让所有的请求都去走jwt
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", jwtFilter);
shiroFilter.setFilters(filters);
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
4、ShiroConfig中需要Realm
package com.shuang.shiro;
import com.shuang.entity.User;
import com.shuang.service.UserService;
import com.shuang.utils.JwtUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
UserService userService;
// 支持的是jwtToken而不是token
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
// 授权
// 拿到用户获取权限信息然后封装成Authorization返回给我们的Shiro
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
5、创建JwtFilter
JwtFilter继承了AuthenticatingFilter 实现了基本登录的逻辑
在通过拦截jwt,并进行校验,我们需要创建JwtUtils工具类
下面截图中login最终走的是Realm类,进行token的认证和授权
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7VypcLoF-1619401649104)(VueBlog.assets/image-20210425085125732.png)]
package com.shuang.shiro;
import cn.hutool.json.JSONUtil;
import com.shuang.common.lang.Result;
import com.shuang.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtFilter extends AuthenticatingFilter {
@Autowired
JwtUtils jwtUtils;
// 用户登录完成后会返回一个jwt给用户
// createToken 主要是调用login方法,并且拿到token,在通过shiro进行认证处理,主要在自定义的Realm中进行身份认证和授权,doGetAuthorizationInfo,doGetAuthenticationInfo
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest)servletRequest;
String jwt = request.getHeader("Authorization");
if (StringUtils.isEmpty(jwt)){
return null;
}
return new JwtToken(jwt);
}
// 拦截 获取到jwt之后是否过期或者凭证不对的细节校验
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//
HttpServletRequest request = (HttpServletRequest)servletRequest;
String jwt = request.getHeader("Authorization");
// 如果token是空的,让其直接去访问xxxController,但是有通过@RequireRole这么一个注解进行权限过滤,如果有些xxxController没有这样一个注解,则说明是一个公共页面,比如注册页面,登录页面
if (StringUtils.isEmpty(jwt)){
return true;
}else {
// 效验jwt
Claims claim = jwtUtils.getClaimByToken(jwt);
// 校验jwt是否为空,或者过期
if (claim==null||jwtUtils.isTokenExpired(claim.getExpiration())) {
throw new ExpiredCredentialsException("token已失效,请重新登录");
}
// jwt不为空且状态正常我们将进行登录处理
// 这个executeLogin会拿到我们的token信息,去交给我们自定义的Realm进行登录Subject.login(token),最终会走到我们自定义Realm的goGetAuthenticationInfo(),在executeLogin中对token进行subject.login(token),就会走OnLoginFailure这样一个异常方法
return executeLogin(servletRequest,servletResponse);
}
}
// 在走到这个方法会抛出异常,而我们是一个前后端分离的项目,而且我们抛出的这个数据是有一定格式的,是Result的一个格式,所以当jwt的内容或者状态出现异常的时候,我们要对onLoginFailure进行重写,封装成result的格式抛出这个异常
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Throwable throwable = e.getCause() == null ? e : e.getCause();
Result result = Result.fail(throwable.getMessage());
String json = JSONUtil.toJsonStr(result);
try {
httpServletResponse.getWriter().print(json);
} catch (IOException ioException) {
}
return false;
}
}
@Bean
JwtFilter jwtFilter() {
return new JwtFilter();
}
需要将Jwt的信息保存到Token里面
6、自定义JwtToken
package com.shuang.shiro;
import org.apache.shiro.authc.AuthenticationToken;
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
7、自定义JwtUtils工具类
定义好了jwtUtils工具类之后我们会在JwtFilter中对jwt进行一个校验处理
package com.shuang.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* jwt工具类
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "shuang.jwt")
public class JwtUtils {
// 密钥
private String secret;
// 超时时间
private long expire;
// 信息
private String header;
/**
* 生成jwt token
*/
public String generateToken(long userId) {
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId+"")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimByToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
log.debug("validate is token error ", e);
return null;
}
}
/**
* token是否过期
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
}
8、添加jwt的配置文件
shuang:
jwt:
# 加密钥匙
secret: f4e2e52034348f86b67cde581c0f9eb5
# token 有效时长,7天 单位秒
expire: 604800
header: Authorization
9、shiro-redis补充
application.yml添加shiro-redis的配置
shiro-redis:
enable: true
reids-manager:
host: 192.168.3.64:6379