本文的github地址为:https://github.com/guanghuichen/shiro-jwt-redis
疫情期间在家闲的无聊,学习一下shiro和JWT的使用,平时工作中虽然一直用到但是始终没有从零到一搭建过,本文基于该github地址https://github.com/dolyw/ShiroJwt的学习文章,本文增加了swagger的本地调试。通过swagger的配置后可以灵活的进行调试代码的正确性。如有不正确的地方烦请指出。
本文有以下几个步骤
-
RBCA基础表的建立,entity。mapper的编写
-
redis的配置以及工具类的编写
-
编写JWTtoken
-
编写JWTutil生成签名以及解密信息
-
编写JWTFilter
-
UserRealm的编写
-
shiroConfig的编写
一:数据库表的建立,此处只提供最简单的表结构。后期根据个人需求拓展即可
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`action` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`status` tinyint(1) DEFAULT 1 COMMENT '0:失效 1:生效',
`update_time` timestamp(0) DEFAULT NULL,
`create_time` timestamp(0) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`status` tinyint(1) DEFAULT 1 COMMENT '0:失效 1:生效',
`update_time` timestamp(0) DEFAULT NULL,
`create_time` timestamp(0) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
`role_id` int(11) NOT NULL,
`permission_id` int(11) NOT NULL,
`create_time` timestamp(0) DEFAULT NULL,
INDEX `role_permission_index`(`role_id`, `permission_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`status` tinyint(1) DEFAULT 1 COMMENT '0:失效 1:生效',
`update_time` timestamp(0) DEFAULT NULL,
`create_time` timestamp(0) DEFAULT NULL,
`account` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '账号信息',
PRIMARY KEY (`id`) USING BTREE,
INDEX `user_name_index`(`name`) USING BTREE,
INDEX `create_time_index`(`create_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
`create_time` timestamp(0) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `user_role_index`(`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
二:redis的配置以及工具类的编写
此处只放配置类。工具类请直接看github地址即可
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* 自定义key(消息队列 暂时用不到 自行忽略)
* 此方法将会根据类名+方法名+所有参数的值生成唯一的一个key,即使@Cacheable中的value属性一样,key也会不一样。
* @Author 科帮网
* @return
* @Date 2017年8月13日
* 更新日志
* 2017年8月13日 科帮网 首次创建
*
*/
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method,
Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
/**
* 缓存管理器
* @Author 科帮网
* @param redisTemplate
* @return CacheManager
* @Date 2017年8月13日
* 更新日志
* 2017年8月13日 科帮网 首次创建
*/
// @SuppressWarnings("rawtypes")
// @Bean
// public CacheManager cacheManager(RedisTemplate redisTemplate) {
// return new RedisCacheManager(redisTemplate);
// }
/**
* 序列化Java对象
* @Author 科帮网
* @param factory
* @return RedisTemplate<Object, Object>
* @Date 2017年8月13日
* 更新日志
* 2017年8月13日 科帮网 首次创建
*
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
template.setConnectionFactory(connectionFactory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
三:编写JWTtoken
JWT核心部分。后期会用到
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;
}
}
四:编写JWTutil生成签名以及解密信息
在这里我们先要解决@Value()注入static参数的问题
private static String encryptJWTKey;
@Value("${encryptJWTKey}")
public void setEncryptJWTKey(String encryptJWTKey) {
JwtUtil.encryptJWTKey = encryptJWTKey;
}
然后重要的部分是sign与verify这两个方法,这个方法使用@Component提前注入或者不注入都可以。网上有好几个版本,我选择注入的版本
@Component
public class JwtUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
/**
* 过期时间
*/
private static String accessTokenExpireTime;
/**
* JWT认证加密私钥(Base64加密)
*/
private static String encryptJWTKey;
/**
* 解决@Value不能修饰static的问题
*/
@Value("${accessTokenExpireTime}")
public void setAccessTokenExpireTime(String accessTokenExpireTime) {
JwtUtil.accessTokenExpireTime = accessTokenExpireTime;
}
@Value("${encryptJWTKey}")
public void setEncryptJWTKey(String encryptJWTKey) {
JwtUtil.encryptJWTKey = encryptJWTKey;
}
/**
*
* @Title: verify @Description: TODO(检验token是否有效) @param: @param
* token @param: @return @return: boolean @throws
*/
public static boolean verify(String token) {
try {
// 通过token获取密码
String secret = getClaim(token, CommonConstant.ACCOUNT) + Base64ConvertUtil.encode(encryptJWTKey);
// 进行二次加密
Algorithm algorithm = Algorithm.HMAC256(secret);
// 使用JWTVerifier进行验证解密
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
return true;
} catch (UnsupportedEncodingException e) {
logger.error("JWTToken认证解密出现UnsupportedEncodingException异常:" + e.getMessage());
return false;
}
}
/**
*
* @Title: sign @Description: TODO(生成签名) @param: @param account @param: @param
* password @param: @param currentTimeMillis @param: @return @return:
* String @throws
*/
public static String sign(String account, String currentTimeMillis) {
try {
// 使用私钥进行加密
String secret = account + Base64ConvertUtil.encode(encryptJWTKey);
// 设置过期时间:根据当前时间计算出过期时间。 此处过期时间是以毫秒为单位,所以乘以1000。
Date date = new Date(System.currentTimeMillis() + Long.parseLong(accessTokenExpireTime) * 1000);
// 对私钥进行再次加密
Algorithm algorithm = Algorithm.HMAC256(secret);
// 生成token 附带account信息
String token = JWT.create().withClaim("account", account).withClaim("currentTimeMillis", currentTimeMillis)
.withExpiresAt(date).sign(algorithm);
return token;
} catch (UnsupportedEncodingException e) {
logger.error("JWTToken加密出现UnsupportedEncodingException异常:" + e.getMessage());
throw new ApiException("JWTToken加密出现UnsupportedEncodingException异常:" + e.getMessage());
}
}
/**
*
* @Title: getClaim @Description:
* TODO(获取token中的信息就是withClaim中设置的值) @param: @param token @param: @param
* claim:sign()方法中withClaim设置的值 @param: @return @return: String @throws
*/
public static String getClaim(String token, String claim) {
try {
// 对token进行解码获得解码后的jwt
DecodedJWT jwt = JWT.decode(token);
// 获取到指定的claim,如果是其他类型返回null
return jwt.getClaim(claim).asString();
} catch (JWTDecodeException e) {
logger.error("解密Token中的公共信息出现JWTDecodeException异常:" + e.getMessage());
throw new ApiException("解密Token中的公共信息出现JWTDecodeException异常:" + e.getMessage());
}
}
}
五:编写JWTFilter
继承了BasicHttpAuthenticationFilter 。该类主要对自己定义的url规则进行过滤。本类方法的执行顺序如下: preHandle->isAccessAllowed->isLoginAttempt->executeLogin
public class JWTFilter extends BasicHttpAuthenticationFilter {
/**
* logger
*/
private static final Logger logger = LoggerFactory.getLogger(JWTFilter.class);
/**
*
* <p>
* Title: preHandle
* </p>
* <p>
* Description:
* </p>
* 对跨域提供支持 Implementation that handles path-matching behavior before a request
* is evaluated. If the path matches and the filter
*
* @param request
* @param response
* @return
* @throws Exception
* @see org.apache.shiro.web.filter.PathMatchingFilter#preHandle(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse)
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(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"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* <p>
* Title: isAccessAllowed
* </p>
* <p>
* Description:
* </p>
* 这里是对http请求进行处理 * 这里我们详细说明下为什么最终返回的都是true,即允许访问 例如我们提供一个地址 GET /article
* 登入用户和游客看到的内容是不同的 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
* 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
* 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
*
* @param request
* @param response
* @param mappedValue
* @return
* @see org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter#isAccessAllowed(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse, java.lang.Object)
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 先对当前请求的URI进行判断是否放行
HttpServletRequest req = (HttpServletRequest) request;
String requestURI = req.getRequestURI();
if (ReleaseAddressUtil.confirm(requestURI)) {// 对于不用进行验证的接口直接放行
return true;
}
if (isLoginAttempt(request, response)) {// 有token
// 进行Shiro的登录UserRealm
try {
this.executeLogin(request, response);
} catch (Exception e) {// 这里对登录异常进行处理
// 认证出现异常,传递错误信息msg
String msg = e.getMessage();
// 获取应用异常(该Cause是导致抛出此throwable(异常)的throwable(异常))
Throwable throwable = e.getCause();
if (throwable instanceof SignatureVerificationException) {
// 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
msg = "Token或者密钥不正确(" + throwable.getMessage() + ")";
} else if (throwable instanceof TokenExpiredException) {
// 该异常为JWT的AccessToken已过期,判断RefreshToken未过期就进行AccessToken刷新
// 刷新token
if (refreshToken(request, response)) {
return true;
} else {
msg = "Token已过期(" + throwable.getMessage() + ")";
}
return true;
} else {
// 应用异常不为空
if (throwable != null) {
// 获取应用异常msg
msg = throwable.getMessage();
}
}
this.response401(request, response, msg);
return false;
}
} else {// 没有token
// 没有携带Token
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
// 获取当前请求类型
String httpMethod = httpServletRequest.getMethod();
// 获取当前请求URI
logger.info("当前请求 {} Authorization属性(Token)为空 请求类型 {}", requestURI, httpMethod);
// mustLoginFlag = true 开启任何请求必须登录才可访问
if (!Config.mustLoginFlag) {// 是否开放游客权限,此处见公共配置类
this.response401(httpServletRequest, response, "请先登录");
return false;
}
}
return true;
}
/**
* <p>
* Title: isLoginAttempt
* </p>
* <p>
* Description:
* </p>
* 检测header里是否含有Authorization字段。
*
* @param request
* @param response
* @return
* @see org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter#isLoginAttempt(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse)
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
String token = this.getAuthzHeader(request);// 返回header中Authorization对应的token的值
return token != null;
}
/**
* <p>
* Title: executeLogin
* </p>
* <p>
* Description:
* </p>
* 进行AccessToken登录认证授权
*
* @param request
* @param response
* @return
* @throws Exception
* @see org.apache.shiro.web.filter.authc.AuthenticatingFilter#executeLogin(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse)
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
// 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
JWTToken token = new JWTToken(this.getAuthzHeader(request));
// 提交给UserRealm进行认证,如果错误他会抛出异常并被捕获
this.getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* @Title: refreshToken @Description:
* TODO(刷新token,具体原理:先查看redis是否存在该key。如果存在取出时间。然后与AccessToken中的时间进行比对。相同则进行更新redis中key的失效时间。提交给shiro进行再次登录,将新生成的token写入到response中返回) @param: @param
* request @param: @param response @param: @return @return:
* boolean @throws
*/
private boolean refreshToken(ServletRequest request, ServletResponse response) {
// 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
String token = this.getAuthzHeader(request);
// 获取当前Token的帐号信息
String account = JwtUtil.getClaim(token, CommonConstant.ACCOUNT);
// 判断Redis中RefreshToken是否存在
if (RedisUtil.hasKey(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
// Redis中RefreshToken还存在,获取RefreshToken的时间戳
String currentTimeMillisRedis = RedisUtil.get(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account);
// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
if (JwtUtil.getClaim(token, CommonConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
// 获取当前最新时间戳
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
// 读取配置文件,获取refreshTokenExpireTime属性
PropertiesUtil.readProperties("application.yml");
String refreshTokenExpireTime = PropertiesUtil.getProperty("refreshTokenExpireTime");
// 设置RefreshToken中的时间戳为当前最新时间戳,且刷新过期时间重新为30分钟过期(配置文件可配置refreshTokenExpireTime属性)
RedisUtil.setEx(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account, currentTimeMillis,
Long.parseLong(refreshTokenExpireTime), TimeUnit.SECONDS);
// 刷新AccessToken延长过期时间,设置时间戳为当前最新时间戳
token = JwtUtil.sign(account, currentTimeMillis);
// 将新刷新的AccessToken再次进行Shiro的登录
JWTToken jwtToken = new JWTToken(token);
// 提交给userRealm进行认证,如果错误他会抛出异常并被捕获,如果没有抛出异常则代表登入成功,返回true
this.getSubject(request, response).login(jwtToken);
// 将token刷入response的header中
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Authorization", token);
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
return true;
}
}
return false;
}
// 缺少权限内部转发至401处理
private void response401(ServletRequest request, ServletResponse response, String msg) {
HttpServletRequest req = (HttpServletRequest) request;
try {
req.getRequestDispatcher("/user/401").forward(request, response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
六:UserRealm的编写
自定义的realm。可以完成对用户权限的写入和验证
@Service
//@Component //经测试两个注解在此处的用户相同
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Autowired
private PermissionMapper permissionMapper;
@Autowired
private RoleMapper roleMapper;
/**
* <p>
* Title: supports
* </p>
* <p>
* Description: 大坑,必须重写此方法,不然Shiro会报错
* </p>
*
* @param token
* @return
* @see org.apache.shiro.realm.AuthenticatingRealm#supports(org.apache.shiro.authc.AuthenticationToken)
*/
@Override
public boolean supports(AuthenticationToken authenticationToken) {
return authenticationToken instanceof JWTToken;
}
/**
* <p>
* Title: doGetAuthorizationInfo
* </p>
* <p>
* Description: 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
* </p>
*
* @param principals
* @return
* @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 从PrincipalCollection中获取token进行验证
String account = JwtUtil.getClaim(principals.toString(), CommonConstant.ACCOUNT);
UserDto userDto = new UserDto();
userDto.setAccount(account);
// 通过账号获取到该用户的角色信息
List<RoleDto> roleList = roleMapper.findRoleListByAccount(userDto);
// 遍历角色信息
for (RoleDto roleDto : roleList) {
if (null != roleDto) {
// 添加该用户的角色信息
simpleAuthorizationInfo.addRole(roleDto.getName());
// 根据用户角色查询权限
List<PermissionDto> permissionDtos = permissionMapper.findPermissionByRole(roleDto);
for (PermissionDto permissionDto : permissionDtos) {
if (null != permissionDto) {
// 添加权限
simpleAuthorizationInfo.addStringPermission(permissionDto.getAction());
}
}
}
}
return simpleAuthorizationInfo;
}
/**
* <p>
* Title: doGetAuthenticationInfo
* </p>
* <p>
* Description:默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
* </p>
*
* @param token
* @return
* @throws AuthenticationException
* @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
// 获取到token
String token = (String) authenticationToken.getCredentials();
// 获取到token信息用于和数据库对比
String account = JwtUtil.getClaim(token, CommonConstant.ACCOUNT);
if (StringUtil.isBlank(account)) {
throw new AuthenticationException("Token中帐号为空(The account in Token is empty.)");
}
// 去数据库查询用户是否存在
UserEntity userDto = new UserDto();
userDto.setAccount(account);
userDto = userMapper.selectOne(userDto);
if (userDto == null) {
throw new AuthenticationException("该帐号不存在(The account does not exist.)");
}
// 开始认证,要AccessToken认证通过,且Redis中存在RefreshToken,且两个Token时间戳一致
if (JwtUtil.verify(token) && RedisUtil.hasKey(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
// 获取RefreshToken的时间戳
// Redis中RefreshToken还存在,获取RefreshToken的时间戳
String currentTimeMillisRedis = RedisUtil.get(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account);
// 获取AccessToken时间戳,与RefreshToken的时间戳对比
if (JwtUtil.getClaim(token, CommonConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
return new SimpleAuthenticationInfo(token, token, "userRealm");
}
}
throw new AuthenticationException("Token已过期(Token expired or incorrect.)");
}
}
七:shiroConfig的编写
shiro配置相关。添加自己的过滤规则。
@Configuration
public class ShiroConfig {
/**
* @Title: defaultWebSecurityManager @Description:
* TODO(配置使用自定义Realm,关闭Shiro自带的session) @param: @param userRealm
* 自定义的Realm @param: @return @return: DefaultWebSecurityManager @throws
*/
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Bean("securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(UserRealm userRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 使用自定义Realm
defaultWebSecurityManager.setRealm(userRealm);
// 关闭Shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
// 设置自定义Cache缓存
defaultWebSecurityManager.setCacheManager(new UserCacheManager());
return defaultWebSecurityManager;
}
/**
* @Title: shiroFilterFactoryBean @Description:
* TODO(添加自己的过滤器规则--详情见文档shiroRules) @param: @param
* securityManager @param: @return @return: ShiroFilterFactoryBean @throws
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器取名为jwt
Map<String, Filter> filterMap = new HashMap<>(16);
filterMap.put("jwt", new JWTFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
// 自定义url规则使用LinkedHashMap有序Map
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(16);
// Swagger接口文档
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
// 所有请求通过我们自己的JWTFilter
filterChainDefinitionMap.put("/**", "jwt");
// if (Config.shiroConfig) {
// filterChainDefinitionMap.put("/**", "jwt"); // 放行所有
// } else {
// filterChainDefinitionMap.put("/login/**", "anon");
// }
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题,https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
八:controller,service的编写
@RestController
@RequestMapping("/user")
@ApiOperation(value = "用户管理",notes = "user manager")
public class UserController {
@Autowired
private UserService userService;
@ApiOperation(value = "统一报错接口",notes = "error")
@RequestMapping(value = "/401",method = RequestMethod.POST)
@ResponseBody
public ResponseBean login401() {
return new ResponseBean(401,"Authc ERROR",false);
}
@ApiOperation(value = "用户登录接口",notes = "Login")
@RequestMapping(value = "/login",method = RequestMethod.POST)
@ResponseBody
public ResponseBean login(@RequestBody UserPo user, HttpServletResponse httpServletResponse) {
if(StringUtil.isBlank(user.getAccount())||StringUtil.isBlank(user.getPassword())) {
return new ResponseBean(0,"用户名或者密码为空",false);
}
return userService.login(user,httpServletResponse);
}
@ApiOperation(value = "用户登录接口",notes = "Login")
@RequiresRoles(value = "admin")//--通过角色来控制访问权限。管理员可以访问
@RequestMapping(value = "/userList",method = RequestMethod.POST)
@ResponseBody
public ResponseBean userList(@RequestBody UserPo user) {
return userService.userList();
}
@ApiOperation(value = "根据角色所绑定的权限去启用接口",notes = "Login")
@RequiresPermissions(value = "user:add")
@RequestMapping(value = "/testPermission",method = RequestMethod.POST)
@ResponseBody
public ResponseBean testPermission(@RequestBody UserPo user) {
return new ResponseBean(100,"你所在的权限拥有 {user:add} 页面权限 ",true);
}
}
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Value("${refreshTokenExpireTime}")
private String refreshTokenExpireTime;
// @Autowired
// private RedisUtil redisUtil;
@Override
public ResponseBean login(UserPo user, HttpServletResponse httpServletResponse) {
// 对密码进行加密
String password = MD5.GetMD5Code(user.getPassword());
UserEntity userEntity = new UserEntity();
userEntity.setAccount(user.getAccount());
userEntity.setPassword(password);
UserEntity result = userMapper.selectOne(userEntity);
if (null == result) {
return new ResponseBean(0, "用户名或者密码错误", false);
}
// 清除可能存在的Shiro权限信息缓存
if (RedisUtil.hasKey(CommonConstant.PREFIX_SHIRO_CACHE + user.getAccount())) {
// 删除
RedisUtil.delete(CommonConstant.PREFIX_SHIRO_CACHE + user.getAccount());
}
// 设置RefreshToken,时间戳为当前时间戳,直接设置即可(不用先删后设,会覆盖已有的RefreshToken)
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
RedisUtil.setEx(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + user.getAccount(), currentTimeMillis,
Long.parseLong(refreshTokenExpireTime), TimeUnit.SECONDS);
// 使用jwt进行登录
String token = JwtUtil.sign(user.getAccount(), currentTimeMillis);
// 从Header中Authorization返回AccessToken,时间戳为当前时间戳
httpServletResponse.setHeader("Authorization", token);
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
return new ResponseBean(HttpStatus.OK.value(), "登录成功(Login Success.)", null);
}
@Override
public ResponseBean userList() {
List<UserEntity> selectAll = userMapper.selectAll();
return new ResponseBean(HttpStatus.OK.value(), "查询成功", selectAll);
}
}