三大核心组件:Subject、SecurityManager、Realm
总结:
应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。
在springboot中整合shiro、redis和jwt,核心的配置:ShiroConfig、JwtFilter、ShiroRealm
,其中jwt主要是负责生成token的工具,redis负责缓存token。
首先我们配置Realm,然后配置filter及jwt工具类,再用shiroConfig来将这些配置联系起来,组成完整的认证鉴权系统。
springboot版本
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.5.RELEASEversion>
<relativePath/>
parent>
jwt和shiro版本
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.11.0version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-spring-boot-starterartifactId>
<version>1.7.1version>
dependency>
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redisartifactId>
<version>3.1.0version>
<exclusions>
<exclusion>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
exclusion>
<exclusion>
<groupId>com.puppycrawl.toolsgroupId>
<artifactId>checkstyleartifactId>
exclusion>
exclusions>
dependency>
ShiroRealm
主要负责认证(AuthenticationInfo)和鉴权(AuthorizationInfo)代码逻辑的实现。
/**
* 认证
*
* @author zwj
*/
@Slf4j
@Component
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private IUserService userService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
return info;
}
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String accessToken = (String) token.getPrincipal();
if (accessToken == null) {
throw new AuthenticationException(CommonCode.WEB_TOKEN_NULL.getMessage());
}
// 校验token有效性
User tokenEntity = this.checkUserTokenIsEffect(accessToken);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(tokenEntity, accessToken, getName());
return info;
}
/**
* 校验token的有效性
* springboot2.3.+新增了一个配置项server.error.includeMessage,默认是NEVER,
* 因此默认是不是输出message的,只要开启就可以了,否则无法拿到shiro抛出异常信息message
* @param token
*/
public User checkUserTokenIsEffect(String token) throws AuthenticationException {
// 解密获得username,用于和数据库进行对比
String userId = JwtUtil.getUserId(token);
if (userId == null) {
throw new AuthenticationException(CommonCode.WEB_TOKEN_ILLEGAL.getMessage());
}
// 查询用户信息
User loginUser = userService.getById(userId);
if (loginUser == null) {
throw new AuthenticationException(CommonCode.WEB_USER_NOT_EXIST.getMessage());
}
// 判断用户状态
if (loginUser.getStatus() != 0) {
throw new LockedAccountException(CommonCode.WEB_ACCOUNT_LOCKED.getMessage());
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, userId, loginUser.getUserPhone())) {
throw new IncorrectCredentialsException(CommonCode.WEB_TOKEN_FAILURE.getMessage());
}
return loginUser;
}
/**
* JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
* 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
* 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
* 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
* 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
* 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
* 用户过期时间 = Jwt有效时间 * 2。
*
* @param userId
* @param userPhone
* @return
*/
public boolean jwtTokenRefresh(String token, String userId, String userPhone) {
//如果缓存中的token为空,直接返回失效异常
String cacheToken = stringRedisTemplate.opsForValue().get(CommonConstant.PREFIX_USER_TOKEN + token);
if (!StrUtils.isBlank(cacheToken)) {
// 校验token有效性
if (!JwtUtil.verify(cacheToken, userId, userPhone)) {
JwtUtil.sign(userId, userPhone);
}
return true;
}
return false;
}
/**
* 清除当前用户的权限认证缓存
*
* @param principals 权限信息
*/
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
}
JwtFilter
这里会拦截需要认证和鉴权的请求,同时会捕获相应异常并抛出
/**
* 过滤器
*
* @author zwj
*/
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 功能描述: 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return boolean
* @author zhouwenjie
* @date 2021/12/24 14:45
*/
@SneakyThrows
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
throw new AuthenticationException(e.getMessage(), e);
}
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = JwtUtil.getTokenByRequest(httpServletRequest);
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
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);
}
}
JwtToken
/**
* token
*
* @author Mark [email protected]
*/
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
public JwtToken(String token){
this.token = token;
}
@Override
public String getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
JwtUtil:token工具类
/**
* @Author zwj
* @Desc JWT工具类
**/
public class JwtUtil {
// Token过期时间180天(用户登录过期时间是此时间的两倍,以token在reids缓存时间为准)
public static final long EXPIRE_TIME = 24 * 180 * 60 * 60 * 1000;
public static final int days = 360;
private static StringRedisTemplate stringRedisTemplate = SpringContextUtils.getBean(StringRedisTemplate.class);
/**
* 校验token是否正确
*
* @param token 密钥
* @param userPhone 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String userId, String userPhone) {
try {
// 根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(userPhone);
JWTVerifier verifier = JWT.require(algorithm).withClaim("userId", userId).build();
// 效验TOKEN
verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 获得token中的信息无需secret解密也能获得
*
* @return token中包含的用户名
*/
public static String getUserId(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("userId").asString();
} catch (Exception e) {
return null;
}
}
/**
* 生成签名,360天后过期
*
* @param userId 用户id
* @param userPhone 用户的密码
* @return 加密的token
*/
public static String sign(String userId, String userPhone) {
// Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(userPhone);
// 附带userId信息 可以将user信息转成map存到这里
// String token = JWT.create().withClaim("userId", userId).withExpiresAt(date).sign(algorithm);
String token = JWT.create().withClaim("userId", userId).sign(algorithm);
stringRedisTemplate.opsForValue().set(CommonConstant.PREFIX_USER_TOKEN + token, token, days, TimeUnit.DAYS);
return token;
}
/**
* 根据request中的token获取用户账号
*
* @param request
* @return
*/
public static String getUserIdByToken(HttpServletRequest request) {
String accessToken = getTokenByRequest(request);
String userId = getUserId(accessToken);
return userId;
}
/**
* 获取 request 里传递的 token
*
* @param request
* @return
*/
public static String getTokenByRequest(HttpServletRequest request) {
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
return token;
}
}
过期时间根据自己需求设定。
ShiroConfig
整合各项配置的联系,注意新版本和老版本的配置区别,新版本需要重新注入beanDefaultAdvisorAutoProxyCreator、AuthorizationAttributeSourceAdvisor
,原因代码中也有详细注释。
/**
* Shiro配置
*
* @author zwj
*/
@Configuration
public class ShiroConfig {
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory();
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/web/login/**", "anon");
filterMap.put("/web/carOwner/list", "anon");
filterMap.put("/web/passengers/list", "anon");
filterMap.put("/web/user/sendSms", "anon");
filterMap.put("/web/sysDictionary/queryByIds", "anon");
filterMap.put("/web/user/addActive", "anon");
filterMap.put("/web/sysNotice/list", "anon");
filterMap.put("/web/sysAds/addViewNum", "anon");
filterMap.put("/web/sysAds/list", "anon");
filterMap.put("/web/sysAds/queryById", "anon");
filterMap.put("/web/sysArea/list", "anon");
//-------防止api文档被过滤掉
filterMap.put("/doc.html", "anon");
filterMap.put("/**/*.js", "anon");
filterMap.put("/**/*.css", "anon");
filterMap.put("/**/*.html", "anon");
filterMap.put("/**/*.svg", "anon");
filterMap.put("/**/*.pdf", "anon");
filterMap.put("/**/*.jpg", "anon");
filterMap.put("/**/*.png", "anon");
filterMap.put("/**/*.ico", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/v2/api-docs-ext", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/", "anon");
//=======防止api文档被过滤掉
filterMap.put("/**", "jwt");
//jwt过滤
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", new JwtFilter());
shiroFilter.setFilters(filters);
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
/**
* 功能描述: 注入realm进行安全管理
*
* @param shiroRealm
* @return org.apache.shiro.web.mgt.DefaultWebSecurityManager
* @author zhouwenjie
* @date 2021/5/5 23:09
*/
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm);
//关闭shiro自带的session存放token功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//使用redis设置自定义缓存token
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
//redis中针对不同用户缓存(此处的id需要对应user实体中的userId字段,用于唯一标识)
redisCacheManager.setPrincipalIdFieldName("id");
//用户权限信息缓存时间
redisCacheManager.setExpire(200000);
return redisCacheManager;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*
* @return
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(lettuceConnectionFactory.getHostName());
redisManager.setPort(lettuceConnectionFactory.getPort());
redisManager.setTimeout(0);
if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
redisManager.setPassword(lettuceConnectionFactory.getPassword());
}
return redisManager;
/*IRedisManager manager;
// redis 单机支持,在集群为空,或者集群无机器时候使用 add by [email protected]
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
RedisManager redisManager = new RedisManager();
redisManager.setHost(lettuceConnectionFactory.getHostName());
redisManager.setPort(lettuceConnectionFactory.getPort());
redisManager.setTimeout(0);
if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
redisManager.setPassword(lettuceConnectionFactory.getPassword());
}
manager = redisManager;
}else{
// redis 集群支持,优先使用集群配置 add by [email protected]
RedisClusterManager redisManager = new RedisClusterManager();
Set portSet = new HashSet<>();
lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
JedisCluster jedisCluster = new JedisCluster(portSet);
redisManager.setJedisCluster(jedisCluster);
manager = redisManager;
}
return manager;*/
}
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
*功能描述: 高版本shrio增加配置,否则类里方法上有@RequiresPermissions注解的,会导致整个类下的接口无法访问404
* @author zhouwenjie
* @date 2021/12/29 9:08
* @param
* @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
/**
* 保存用户
*/
@ApiOperation(value = "保存用户", notes = "保存用户")
@SysLog("保存用户")
@PostMapping("/save")
@RequiresPermissions("sys:user:save")
public Result save(@RequestBody SysUserEntity user){
ValidatorUtils.validateEntity(user, ValidGroups.AddGroup.class);
user.setCreateUserId(getUserId());
sysUserService.saveUser(user);
return Result.ok();
}