1.相关依赖包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.spring.version}</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro.redis.version}</version>
<exclusions>
<exclusion>
<artifactId>shiro-core</artifactId>
<groupId>org.apache.shiro</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.JWT 工具类(校验token,生成token)
@Component
@Slf4j
public class JwtUtil {
/**
* 过期时间
*/
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) {
log.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) {
log.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) {
log.error("解密Token中的公共信息出现JWTDecodeException异常:" + e.getMessage());
throw new ApiException("解密Token中的公共信息出现JWTDecodeException异常:" + e.getMessage());
}
}
}
3.自定义Realm,继承AuthorizingRealm类,因为这个类里面就有实现接收用户认证信息和接收用户权限信息的两个方法,
- Authentication(认证):用户身份识别,通常被称为用户“登录”
- Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
@Service
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private ResourceService resourceService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 认证
*
* @param auth
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
// 获取到token
String token = (String) auth.getCredentials();
String account = JwtUtil.getClaim(token, CommonConstant.ACCOUNT);
if (StringUtils.isEmpty(account)) {
throw new AuthenticationException("token错误!");
}
User user = userService.getByUserName(account);
if (user == null) {
throw new UnknownAccountException("账号不存在!");
}
if (user.getStatus() != null && UserStatusEnum.DISABLE.getCode().equals(user.getStatus())) {
throw new LockedAccountException("帐号已被锁定,禁止登录!");
}
try {
// 开始认证,要AccessToken认证通过,且Redis中存在RefreshToken,且两个Token时间戳一致
if (JwtUtil.verify(token) && RedisUtil.hasKey(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
// Redis中RefreshToken还存在,获取RefreshToken的时间戳
Object currentTimeMillisRedis = RedisUtil.get(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account);
// 获取AccessToken时间戳,与RefreshToken的时间戳对比
if (JwtUtil.getClaim(token, CommonConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis.toString())) {
return new SimpleAuthenticationInfo(token, token, "userRealm");
} else {
throw new AuthenticationException("token认证失败!");
}
} else {
throw new AuthenticationException("token认证失败!");
}
} catch (TokenExpiredException e) {
throw new AuthenticationException("token已过期!");
} catch (SignatureVerificationException e) {
throw new AuthenticationException("密码不正确!");
}
}
/**
* 权限认证,为当前登录的Subject授予角色和权限(角色的权限信息集合)
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 从PrincipalCollection中获取token进行验证
String account = JwtUtil.getClaim(principals.toString(), CommonConstant.ACCOUNT);
if (StringUtils.isNotEmpty(account)) {
User user = userService.getByUserName(account);
//账户禁用
if (user.getStatus() == 0) {
throw new LockedAccountException();
}
if (user != null) {
List<Role> roleList = roleService.getUserRoles(user.getId());
for (Role role : roleList) {
simpleAuthorizationInfo.addRole(role.getName());
}
}
// 赋予权限
/*List<Resource> resourcesList = null;
// ROOT用户默认拥有所有权限
if (CommonConstants.ROOT.equalsIgnoreCase(user.getUserName())) {
resourcesList = resourceService.getAllList();
} else {
resourcesList = resourceService.listByUserId(user.getId());
}*/
List<Resource> resourcesList = resourceService.listByUserId(user.getId());
if (!CollectionUtils.isEmpty(resourcesList)) {
Set<String> permissionSet = new HashSet<>();
for (Resource resources : resourcesList) {
String permission = null;
if (!StringUtils.isEmpty(permission = resources.getPermission())) {
permissionSet.addAll(Arrays.asList(permission.trim().split(",")));
}
}
simpleAuthorizationInfo.setStringPermissions(permissionSet);
}
}
return simpleAuthorizationInfo;
}
}
4.JWT拦截器
/**
* jwt过滤器
*
* @description:
* @author:
* @time:
*/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Value("${refreshTokenExpireTime}")
private String refreshTokenExpireTime;
/**
* 如果带有 token,则对 token 进行检查,否则直接通过
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
// 先对当前请求的URI进行判断是否放行
HttpServletRequest req = (HttpServletRequest) request;
String requestURI = req.getRequestURI();
//对于不用进行验证的接口直接放行
if (ReleaseAddressUtil.confirm(requestURI)) {
return true;
}
/* if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
//token 错误
responseError(response, e.getMessage());
return false; //产生异常则阻止请求的继续执行
}
}
//如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
return true;*/
//判断请求的请求头是否带上 "Token"
if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
executeLogin(request, response);
return true;
} 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
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
// 获取当前请求类型
String httpMethod = httpServletRequest.getMethod();
// 获取当前请求URI
log.info("当前请求 {} Authorization属性(Token)为空 请求类型 {}", requestURI, httpMethod);
// mustLoginFlag = true 开启任何请求必须登录才可访问
this.response401(httpServletRequest, response, "请先登录");
return false;
}
//如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
// return true;
}
/**
* 判断用户是否想要登入。
* 检测 header 里面是否包含 Token
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(AUTHORIZATION);
return token != null;
}
/**
* 执行登陆操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(AUTHORIZATION);
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 对跨域提供支持,如果nginx配置了或者网关配置了,不需要再配置
*/
// @Override
// protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
// HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// httpServletResponse.setHeader("Access-control-Allow-Origin", "*");
// httpServletResponse.setHeader("Access-Control-Allow-Methods", "*");
// httpServletResponse.setHeader("Access-Control-Allow-Headers", "*");
// // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
// if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
// httpServletResponse.setStatus(HttpStatus.OK.value());
// return false;
// }
// return super.preHandle(request, response);
// }
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的时间戳
Object currentTimeMillisRedis = RedisUtil.get(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account);
// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
if (JwtUtil.getClaim(token, CommonConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis.toString())) {
// 获取当前最新时间戳
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
// 设置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处理
*
* @param request
* @param response
* @param msg
*/
// 缺少权限内部转发至401处理
private void response401(ServletRequest request, ServletResponse response, String msg) {
HttpServletRequest req = (HttpServletRequest) request;
try {
req.getRequestDispatcher("/user/unauthorized?message=" + msg).forward(request, response);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 请求异常跳转
*/
private void responseError(ServletResponse response, String message) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//设置编码,否则中文字符在重定向时会变为空字符串
message = URLEncoder.encode(message, "UTF-8");
httpServletResponse.sendRedirect("/user/noLogin?message=" + message);
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
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;
}
}
5.自定义要放行的接口
public class ReleaseAddressUtil {
private static Set<String> getInterface() {
Set<String> set = new HashSet<String>();
set.add("/user/login");
set.add("/user/logout");
set.add("/user/noLogin");
// 所有请求通过我们自己的JWT Filter
return set;
}
public static Boolean confirm(String requestURI) {
Set<String> set = getInterface();
return set.contains(requestURI);
}
}
6.shiro核心配置
@Configuration
public class ShiroConfig {
@Bean("securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 使用自己的realm
securityManager.setRealm(userRealm);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
// 设置自定义Cache缓存
securityManager.setCacheManager(new UserCacheManager());
return securityManager;
}
@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规则
* http://shiro.apache.org/web.html#urls-
*/
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(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");
//没有登录的用户请求需要登录的资源时自动跳转到该路径
factoryBean.setLoginUrl("/user/noLogin");
//没有权限默认跳转
factoryBean.setUnauthorizedUrl("/user/unauthorized");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
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;
}
}
7.登录接口
/**
* 登录
*
* @param userName
* @param password
* @return
*/
@PostMapping("/login")
public RestResponse<Map> userLogin(@RequestParam String userName, @RequestParam String password, HttpServletResponse response) {
User loginUser = userService.getByUserName(userName);
if (loginUser == null) {
return RestResponse.fail("用户名不存在");
}
String pass = Md5Util.GetMD5Code(password);
User user = userService.getUserByNameAndPwd(userName, pass);
if (user == null) {
return RestResponse.fail("用户名或者密码错误");
}
// // 清除可能存在的Shiro权限信息缓存
if (RedisUtil.hasKey(CommonConstant.PREFIX_SHIRO_CACHE + user.getUserName())) {
// 删除
RedisUtil.delete(CommonConstant.PREFIX_SHIRO_CACHE + user.getUserName());
}
// 设置RefreshToken,时间戳为当前时间戳,直接设置即可(不用先删后设,会覆盖已有的RefreshToken)
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
RedisUtil.setEx(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + user.getUserName(), currentTimeMillis,
Long.parseLong(refreshTokenExpireTime), TimeUnit.SECONDS);
// 使用jwt进行登录
String token = JwtUtil.sign(user.getUserName(), currentTimeMillis);
response.setHeader("Authorization", token);
response.setHeader("Access-Control-Expose-Headers", "Authorization");
Map map = new ConcurrentHashMap<>(16);
List<Resource> resources = resourceService.getUserResources(user.getId());
map.put("loginUser", user);
map.put("resources", resources);
map.put("token", token);
map.put("msg", "登录成功");
return RestResponse.success(map);
}