SpringBoot 基于Shiro + Jwt + Redis的用户权限管理 (一) 简介与配置
项目Github地址: https://github.com/baiye21/ShiroDemo
-
SpringBoot 基于Shiro + Jwt + Redis的用户权限管理 (一) 简介与配置
-
SpringBoot 基于Shiro + Jwt + Redis的用户权限管理 (二) 认证
-
SpringBoot 基于Shiro + Jwt + Redis的用户权限管理 (三) 鉴权
一,大体功能
1.登录成功返回access_token
用户通过用户密码进行login,验证成功后生成一个access_token返回给前端,之后的请求都需要将access_token放在Request Header部Authorization字段中,才能正常访问。
2.后续请求需携带access_token
将自定义的JwtFilter拦截器加入到了Shiro的过滤器中,没有在过滤链中的请求URL,进入拦截器,获取请求Header的access_token,进行shiro的token登录认证授权。
3.Redis保存refresh_token
生成access_token时,有效载荷中加入当前时间戳与用户id,同时往Redis插入一条key为固定前缀加用户id,value为时间戳的refresh_token的记录,过期时间要比access_token长,当发给用户的access_token过期后,查找refresh_token记录,若refresh_token有效,则重新生成access_token返回给前端,之后请求需要携带新的access_token,从而实现access_token的自动刷新。
4.多个Realm
通过继承ModularRealmAuthenticator,实现shiro的多realm认证,正常登录使用PasswordRealm,Token认证使用DemoRealm授权,还可以扩展追加其他方式的认证Realm。
二,Shiro简介
1.ShiroAPI
Shiro不太了解的话,建议看 Shiro官方API ,至少红色框出来的部分耐心浏览一遍,将会对Shiro的认证与鉴权流程有一个大概的了解,这里把比较重要的组成图,认证和授权流程图截取出来放在后面了。
2.Shiro主要组成
- Authentication : 认证
- Authorization : 授权
- Session Management : session 管理
- Cryptography : 密码匹配
3.Authentication 认证流程(doGetAuthenticationInfo)
4.Authorization 授权流程(doGetAuthorizationInfo )
三,项目介绍
一,测试数据库表,
项目只是演示Demo,就不弄用户权限表,角色表之类的了,一张表搞定。表结构如下
CREATE TABLE `m_user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` varchar(20) NOT NULL COMMENT '用户ID',
`user_name` varchar(20) NOT NULL COMMENT '用户名',
`pass_word` varchar(255) NOT NULL COMMENT '密码',
`account_type` char(1) NOT NULL COMMENT '账号类型',
`permission_type` varchar(3) NOT NULL COMMENT '权限类型',
`del_flg` char(1) DEFAULT '0' COMMENT '删除flag',
`reg_account` varchar(20) DEFAULT NULL COMMENT '登录者',
`reg_time` datetime DEFAULT NULL COMMENT '登录时间',
`upd_account` varchar(20) DEFAULT NULL COMMENT '更新者',
`upd_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户表'
二,项目结构
项目结构如下图,
数据库持久层用Mybatis,并使用Mybatis-generator反向生成数据表对应实体类等。
日志用的og4j + slf4j + lombok @Slf4j
pom文件就不贴全部了,有点长了,有需要移步github查看。这里只贴Shiro相关的吧
<!--shiro权限框架-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<!--JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
四,关键配置(说明尽量写在注释里面了)
1.ShiroConfig
package com.demo.config;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import com.demo.filter.JwtFilter;
import com.demo.shiro.RedisCacheManager;
import com.demo.shiro.ShiroSessionManager;
import com.demo.shiro.realm.JwtRealm;
import com.demo.shiro.realm.PasswordRealm;
import com.demo.shiro.realm.DemoRealm;
import com.demo.shiro.realm.UserModularRealmAuthenticator;
/*
* Author : baiye
Time : 2021/06/30
Function: Shiro配置
*/
@Configuration
public class ShiroConfig {
/**
* 密码登录时指定匹配器,
*
* @return HashedCredentialsMatcher
*/
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// 设置哈希算法名称
matcher.setHashAlgorithmName("MD5");
// 设置哈希迭代次数
matcher.setHashIterations(666);
// 设置存储凭证十六进制编码
matcher.setStoredCredentialsHexEncoded(true);
return matcher;
}
/**
* 如果需要密码匹配器则需要进行指定 密码登录Realm
* @return PasswordRealm
*/
@Bean
public PasswordRealm passwordRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
PasswordRealm passwordRealm = new PasswordRealm();
passwordRealm.setCredentialsMatcher(matcher);
return passwordRealm;
}
/**
* jwtRealm
* @return JwtRealm
*/
@Bean
public JwtRealm jwtRealm() {
return new JwtRealm();
}
/**
* demoRealm
* @return DemoRealm
*/
@Bean
public DemoRealm demoRealm() {
return new DemoRealm();
}
/**
* Shiro内置过滤器,可以实现拦截器相关的拦截器
* 常用的过滤器:
* anon:无需认证(登录)可以访问
* authc:必须认证才可以访问
* user:如果使用rememberMe的功能可以直接访问
* perms:该资源必须得到资源权限才可以访问
* role:该资源必须得到角色权限才可以访问
**/
@Bean
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置未登录跳转url
// shiroFilterFactoryBean.setUnauthorizedUrl("/user/unLogin");
Map<String, String> filterMap = new LinkedHashMap<String, String>();
// 只有不需要权限认证(anon)的 需要明确写入filterMap
filterMap.put("/hello", "anon");
filterMap.put("/register/*", "anon");
filterMap.put("/login/*", "anon");
// 添加自定义过滤器并且取名为jwt
Map<String, Filter> filter = new HashMap<String, Filter>(1);
filter.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filter);
// 过滤链定义,从上向下顺序执行,所以放在最为下边
filterMap.put("/**", "jwt");
// 未授权界面返回JSON
// shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
// shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
@Bean
public UserModularRealmAuthenticator userModularRealmAuthenticator() {
// 自己重写的ModularRealmAuthenticator
UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealmAuthenticator;
}
/**
* SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
*/
@Bean
public SecurityManager securityManager(
@Qualifier("passwordRealm") PasswordRealm passwordRealm,
@Qualifier("jwtRealm") JwtRealm jwtRealm,
@Qualifier("demoRealm") DemoRealm demoRealm,
@Qualifier("userModularRealmAuthenticator") UserModularRealmAuthenticator userModularRealmAuthenticator) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm
securityManager.setAuthenticator(userModularRealmAuthenticator);
List<Realm> realms = new ArrayList<>();
// 添加多个realm
realms.add(passwordRealm);
realms.add(jwtRealm);
realms.add(demoRealm);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
// 禁止创建shiro session
// securityManager.setSubjectFactory(subjectFactory());
// 解决第一次访问没有JSESSIONID导致404错误
// securityManager.setSessionManager(mySessionManager());
// 自定义sessionManager
securityManager.setSessionManager(shiroSessionManager());
// 注入记住我管理器
// securityManager.setRememberMeManager(rememberMeManager());
// ★★★★★★★★★★★★★★ 使用自定义的 redisCacheManage ★★★★★★★★★★★★★★
// securityManager.setCacheManager(redisCacheManager());
// securityManager设置自定义认证规则
securityManager.setRealms(realms);
return securityManager;
}
@Bean
public ShiroSessionManager shiroSessionManager() {
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
// TODO redis 配置session持久化
shiroSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
return shiroSessionManager;
}
@Bean
public DefaultWebSessionManager mySessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
// defaultWebSessionManager.setGlobalSessionTimeout(60 * 30 * 1000);
defaultWebSessionManager.setDeleteInvalidSessions(true);
defaultWebSessionManager.setSessionValidationSchedulerEnabled(true);
defaultWebSessionManager.setSessionIdCookieEnabled(true);
// 将sessionIdUrlRewritingEnabled属性设置成false
defaultWebSessionManager.setSessionIdUrlRewritingEnabled(false);
return defaultWebSessionManager;
}
@Bean
public RedisCacheManager redisCacheManager() {
return new RedisCacheManager();
}
/**
* cookie对象; rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
*
* @return
*/
// @Bean
// public SimpleCookie rememberMeCookie() {
//
// // 这个参数是cookie的名称
// SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//
// // <!-- 记住我cookie生效时间30天 ,单位秒;-->
// simpleCookie.setMaxAge(259200);
//
// return simpleCookie;
// }
/**
* cookie管理对象;
* rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
*
* @return
*/
// @Bean
// public CookieRememberMeManager rememberMeManager() {
//
// CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
// cookieRememberMeManager.setCookie(rememberMeCookie());
// // rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
// cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
// return cookieRememberMeManager;
// }
/**
* Shiro生命周期处理器
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
*
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),<br>
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
*
* @return DefaultAdvisorAutoProxyCreator
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
2.ShiroSessionManager
package com.demo.shiro;
import java.io.Serializable;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import com.demo.common.Const;
import com.demo.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
/*
* Author : baiye
Time : 2021/06/30
Function: shiro自定义session管理器
*/
@Slf4j
public class ShiroSessionManager extends DefaultWebSessionManager {
public ShiroSessionManager() {
super();
}
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
// 如果请求头中有 Authorization
String token = WebUtils.toHttp(request).getHeader(Const.TOKEN_HEADER_NAME);
if (!StringUtils.isEmpty(token)) {
if (JwtUtil.verify(token, Const.TOKEN_SECRET)) {
String id = JwtUtil.getClaim(token, Const.JSESSIONID);
log.debug("ShiroSessionManager从http header 取出token中的JSESSIONID:{}", id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
return super.getSessionId(request, response);
} else {
// 否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
3.JwtFilter
package com.demo.filter;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.demo.common.Const;
import com.demo.common.ConstCode;
import com.demo.common.RedisConst;
import com.demo.common.ResponseCode;
import com.demo.common.ServerResponse;
import com.demo.shiro.JwtToken;
import com.demo.util.JsonConverUtil;
import com.demo.util.JwtUtil;
import com.demo.util.RedisUtil;
import com.demo.util.SysTimeUtil;
import lombok.extern.slf4j.Slf4j;
/*
* Author : baiye
Time : 2021/06/30
Function: 核心JWT拦截器,拦截Shiro过滤链anon之外的请求
*/
/**
* 这个类最主要的目的是:当请求需要校验权限,token是否具有权限时,构造出主体subject执行login()
*/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Autowired
RedisUtil redisUtil;
@Autowired
JsonConverUtil jsonConverUtil;
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return 是否成功
*/
@Override
// 这个方法判断 尝试进行登录的操作,如果token存在,那么进行提交登录,如果不存在说明可能是正在进行登录或者做其它的事情 直接放过即可
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
// return false;
// throw new AuthenticationException("Token失效请重新登录");
// 认证出现异常,传递错误信息msg
String msg = e.getMessage();
// 获取应用异常(该Cause是导致抛出此throwable(异常)的throwable(异常))
Throwable throwable = e.getCause();
if (throwable != null && throwable instanceof SignatureVerificationException) {
// 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
msg = "token或者密钥不正确(" + throwable.getMessage() + ")";
} else if (throwable != null && throwable instanceof TokenExpiredException) {
// 该异常为JWT的AccessToken已过期(TokenExpiredException),
// 判断RefreshToken未过期就进行AccessToken刷新
if (this.refreshToken(request, response)) {
return true;
} else {
msg = "token已过期(" + throwable.getMessage() + ")";
}
} else {
// 应用异常不为空
if (throwable != null) {
// 获取应用异常msg
msg = throwable.getMessage();
}
}
/**
* 错误两种处理方式
* 1. 将非法请求转发到/401的Controller处理,抛出自定义无权访问异常被全局捕捉再返回Response信息
* 2. 无需转发,直接返回Response信息 一般使用第二种(更方便)
*/
// 直接返回Response信息
this.response401(request, response, msg);
return false;
}
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
this.sendChallenge(request, response);
return false;
}
/**
* 执行登录
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response)
throws AuthenticationException {
// log.info("进入JwtFilter类中...");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// head部存放的 authorization token
String token = httpServletRequest.getHeader(Const.TOKEN_HEADER_NAME);
// 判断token是否存在
/*
* if (token == null) { return false; }
*/
if (!StringUtils.isNotBlank(token)) {
log.error("获取到的token为空");
throw new AuthenticationException();
}
// log.info("获取到的token是:{}", token);
// JwtToken token = new JwtToken(this.getAuthzHeader(request))
JwtToken jwtToken = new JwtToken(token);
try {
log.debug("提交UserModularRealmAuthenticator查找对应的realm...");
getSubject(request, response).login(jwtToken);
} catch (AuthenticationException e) {
log.debug("捕获到身份认证异常{}", e.getMessage());
throw e;
}
return true;
}
/**
* 刷新AccessToken,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问
*/
private boolean refreshToken(ServletRequest request, ServletResponse response) {
// Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
String token = this.getAuthzHeader(request);
// Token的帐号信息获取
String userid = JwtUtil.getClaim(token, Const.TOKEN_CLAIM_USERID);
// Redis中RefreshToken的key
String refreshTokenKey = RedisConst.PREFIX_SHIRO_REFRESH_TOKEN + userid;
// Redis中RefreshToken是否存在
if (redisUtil.hasKey(refreshTokenKey)) {
// Redis中RefreshToken还存在,获取RefreshToken的时间戳
String currentTimeMillisRedis = redisUtil.get(refreshTokenKey).toString();
// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
if (JwtUtil.getClaim(token, Const.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
// 获取当前最新时间戳
String currentTimeMillis = String.valueOf(SysTimeUtil.getTime());
/**
* ★★★
* 这里重新设置RefreshToken为一天,可能会导致RefreshToken一直续期,
* 可以一开始设置RefreshToken大一些
* 比如为7天,续期时不在重置,这样一次登录的Token Refresh有效期就是一周,一周后必须重新登录。
* ★★★
* */
// 设置RefreshToken中的时间戳为当前最新时间戳,且刷新过期时间
redisUtil.set(refreshTokenKey, currentTimeMillis, Const.REFRESH_TOKEN_EXPIRE_TIME);
// 账号类型
String accountType = JwtUtil.getClaim(token, Const.ACCOUNT_TPYE);
String sessionId = JwtUtil.getClaim(token,Const.JSESSIONID);
// 刷新AccessToken,设置时间戳为当前最新时间戳
token = JwtUtil.loginSign(userid, accountType, currentTimeMillis, sessionId,
Const.TOKEN_SECRET);
// 将新刷新的AccessToken再次进行Shiro的登录
JwtToken jwtToken = new JwtToken(token);
// 提交给DemoRealm进行认证,如果错误他会抛出异常并被捕获,如果没有抛出异常则代表登入成功,返回true
this.getSubject(request, response).login(jwtToken);
// 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader(Const.TOKEN_HEADER_NAME, token);
httpServletResponse.setHeader(Const.TOKEN_ACCESS_CONTROL, Const.TOKEN_HEADER_NAME);
return true;
}
}
return false;
}
/**
* 无需转发,直接返回Response信息
*/
private void response401(ServletRequest req, ServletResponse response, String msg) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
try {
out = httpServletResponse.getWriter();
// object 转 json字符串
String data = JSON.toJSONString(ServerResponse.createByErrorCodeMessage(ResponseCode.UNAUTHORIZED.getCode(),
"UNAUTHORIZED"));
out.append(data);
} catch (IOException e) {
log.error("response401 : {}",e.getMessage());
} finally {
if (out != null) {
out.close();
}
}
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// httpServletResponse.setHeader("Access-control-Allow-Origin",
// httpServletRequest.getHeader("Origin"));
if (StringUtils.isNotEmpty(httpServletRequest.getHeader("Origin"))) {
httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
} else {
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
}
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE,HEAD");
// httpServletResponse.setHeader("Access-Control-Allow-Headers",
// httpServletRequest.getHeader("Access-Control-Request-Headers"));
httpServletResponse.setHeader("Access-Control-Allow-Headers",
"Content-Type,Origin,Accept,Authorization,X-Requested-With,No-Cache,If-Modified-Since,Pragma,Last-Modified,Cache-Control,Expires,X-E4M-With");
// 跨域时会首先发送一个option请求,给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
4.UserModularRealmAuthenticator
package com.demo.shiro.realm;
import java.util.ArrayList;
import java.util.Collection;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import com.demo.common.ConstCode;
import com.demo.shiro.CustomizedToken;
import com.demo.shiro.JwtToken;
import lombok.extern.slf4j.Slf4j;
/*
* Author : baiye
Time : 2021/06/30
Function:
*/
@Slf4j
public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
// 当subject.login()方法执行,下面的代码即将执行
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
log.debug("UserModularRealmAuthenticator:method doAuthenticate() 执行 ");
// 判断getRealms()是否返回为空
assertRealmsConfigured();
// 所有Realm
Collection<Realm> realms = getRealms();
// 盛放登录类型对应的所有Realm集合
Collection<Realm> typeRealms = new ArrayList<>();
if(authenticationToken instanceof JwtToken) {
log.debug("验证的Token类型是:{}", "JwtToken");
typeRealms.clear();
// 获取header部的token进行强制类型转换
JwtToken jwtToken = (JwtToken) authenticationToken;
for (Realm realm : realms) {
if (realm.getName().contains("Demo")) {
typeRealms.add(realm);
}
}
return doSingleRealmAuthentication(typeRealms.iterator().next(), jwtToken);
} else {
typeRealms.clear();
// 这个类型转换的警告不需要再关注 如果token错误 后面将会抛出异常信息
CustomizedToken customizedToken = (CustomizedToken) authenticationToken;
log.debug("验证的Token类型是:{}", "CustomizedToken");
// 登录类型
String loginType = customizedToken.getLoginType();
log.debug("验证的realm类型是:{}", loginType);
for (Realm realm : realms) {
if (realm.getName().contains(loginType)) {
log.debug("当前realm:{}被注入:", realm.getName());
typeRealms.add(realm);
}
}
// 判断是单Realm还是多Realm
if (typeRealms.size() == ConstCode.NUM_1) {
log.debug("一个realm");
return doSingleRealmAuthentication(typeRealms.iterator().next(), customizedToken);
} else {
log.debug("多个realm");
return doMultiRealmAuthentication(typeRealms, customizedToken);
}
}
}
}
5.PasswordRealm(只做认证 doGetAuthenticationInfo,参数AuthenticationToken为自定义的CustomizedToken,认证成功的principal为用户表对应的UserMaster对象实例)
package com.demo.shiro.realm;
import com.demo.entity.UserMaster;
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.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import com.demo.service.IUserService;
import com.demo.shiro.CustomizedToken;
import lombok.extern.slf4j.Slf4j;
/*
* Author : baiye
Time : 2021/06/30
Function: 用户密码认证Realm
*/
@Slf4j
public class PasswordRealm extends AuthorizingRealm {
@Autowired
@Lazy
private IUserService iUserService;
/**
* shiro 赋权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.debug("PasswordRealm 不做赋权处理");
// password login 不做赋权处理
return null;
}
/**
* shiro 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
log.debug("PasswordRealm权限认证开始,传递的token:{}", authenticationToken);
CustomizedToken token = (CustomizedToken) authenticationToken;
log.debug("PasswordRealm转换的自定义token:{}", token);
// 找出数据库中的对象 和用户输入的对象做出对比
// 根据userid查询用户
UserMaster userMaster = iUserService.getUserByUserId(token.getUsername());
if (userMaster == null) {
log.debug("该账号不存在:{}", token.getUsername());
// 抛出账号不存在异常
throw new UnknownAccountException();
}
// 用户密码
Object hashedCredentials = userMaster.getPassWord();
/***
* param1 : 数据库用户<br>
* param2 : 密码<br>
* param3 : 加密所用盐值 <br>
* param4 : 当前realm的名称<br>
*/
return new SimpleAuthenticationInfo(userMaster, hashedCredentials,
ByteSource.Util.bytes(userMaster.getUserId()), getName());
}
}
6.DemoRealm(使用Jwt做认证doGetAuthenticationInfo,认证成功返回的principal是token,所以鉴权doGetAuthorizationInfo时使用token解析出userid去数据库获取用户的角色和权限信息。 )
package com.demo.shiro.realm;
import com.demo.entity.UserMaster;
import org.apache.commons.lang3.StringUtils;
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;
import org.springframework.context.annotation.Lazy;
import com.demo.common.Const;
import com.demo.common.RedisConst;
import com.demo.pojo.common.UserAccessInfo;
import com.demo.service.IUserService;
import com.demo.shiro.JwtToken;
import com.demo.util.JwtUtil;
import com.demo.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
/*
* Author : baiye
Time : 2021/06/30
Function:
*/
/**
* 用户登录鉴权和获取用户授权
*/
@Slf4j
public class DemoRealm extends AuthorizingRealm {
@Autowired
@Lazy
private IUserService iUserService;
@Autowired
@Lazy
private RedisUtil redisUtil;
/**
* 必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 功能: 获取用户权限信息,包括角色以及权限。只有当触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
*
* @param principals token
* @return AuthorizationInfo 权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("demoRealm doGetAuthorizationInfo 用户赋权 ");
String userid = null;
if (principals != null) {
// 此处的principals为 UserMaster
Object PrimaryPrincipal = principals.getPrimaryPrincipal();
if (PrimaryPrincipal instanceof UserMaster) {
UserMaster userMaster = (UserMaster) PrimaryPrincipal;
userid = userMaster.getUserId();
} else {
// 此处的principals为token
userid = JwtUtil.getClaim(principals.toString(), Const.TOKEN_CLAIM_USERID);
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
UserAccessInfo userAccessInfo = iUserService.getUserAccessInfo(userid);
/**
* 设置用户拥有的角色集合,<br>
*
* accountType = 1 管理员 admin <br>
* accountType = 2 领导 Leader <br>
* accountType = 3 普通用户 user <br>
*
*/
info.setRoles(userAccessInfo.getRoleSet());
// 设置用户拥有的权限集合
info.addStringPermissions(userAccessInfo.getPermissionSet());
return info;
}
/**
* 功能: 用来进行身份认证,也就是说验证用户输入的账号和密码是否正确,获取身份验证信息,错误抛出异常
*
* @param auth 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
if (StringUtils.isBlank(token)) {
log.error("token为空,身份认证失败!");
throw new AuthenticationException("token为空!");
}
// 获取token携带的用户名
String userid = JwtUtil.getClaim(token, Const.TOKEN_CLAIM_USERID);
// 帐号为空
if (StringUtils.isBlank(userid)) {
throw new AuthenticationException("token中用户为空(The userid in Token is empty.)");
}
// UserMaster表查询用户
UserMaster userMaster = iUserService.getUserByUserId(userid);
if (userMaster == null) {
throw new AuthenticationException("该帐号不存在(The account does not exist.)");
}
String refreshTokenKey = RedisConst.PREFIX_SHIRO_REFRESH_TOKEN + userid;
// 开始认证,要AccessToken认证通过,且Redis中存在RefreshToken,且两个Token时间戳一致
if (JwtUtil.verify(token, Const.TOKEN_SECRET) && redisUtil.hasKey(refreshTokenKey)) {
// 获取RefreshToken的时间戳
String currentTimeMillisRedis = redisUtil.get(refreshTokenKey).toString();
// 获取AccessToken时间戳,与RefreshToken的时间戳对比
if (JwtUtil.getClaim(token, Const.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
// principal,集成redis时,这里必须为对象,确保key唯一,且pojo实现序列化
// credentials 这里使用token
return new SimpleAuthenticationInfo(token, token, "demoRealm");
}
}
throw new AuthenticationException("token expired or incorrect.");
// 校验token有效性
// UserMaster loginUser = this.checkUserTokenIsEffect(token);
// principal,集成redis时,这里必须为对象,确保key唯一,且pojo实现序列化
// credentials 这里使用token
// return new SimpleAuthenticationInfo(token, token, getName());
}
}