shiro的jwt认证实现
1.环境
- spring boot 版本 2.1.9.RELEASE
- mybatis-plus 版本 3.2.0
<dependencies>
<!--shiro核心-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<!--shiro与spring整合依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.2</version>
</dependency>
</dependencies>
个人学习总结,仅供参考
2.代码
- ShiroConfig shiro主配置类
import cn.hutool.core.collection.CollectionUtil;
import com.f4Blog.auth.interceptor.ShiroJwtFilter;
import com.f4Blog.auth.shiro.ShiroRealm;
import com.f4Blog.basic.util.SpringUtils;
import com.f4Blog.model.constant.LoginConstant;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.session.mgt.DefaultSessionManager;
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.mgt.DefaultWebSessionStorageEvaluator;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Shiro配置类
*/
@Configuration
//要求首先把上下文支持组件注册到spring
@DependsOn("springUtils")
public class ShiroConfig {
/**
* 创建shiro的过滤器工厂bean
* @return
*/
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager")DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return shiroFilterFactoryBean;
}
/**
* 禁用session功能
* @return
*/
@Bean
public DefaultSessionManager sessionManager() {
DefaultSessionManager manager = new DefaultSessionManager();
manager.setSessionValidationSchedulerEnabled(false);
return manager;
}
@Bean
public SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
@Bean
public DefaultSubjectDAO subjectDAO() {
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
return defaultSubjectDAO;
}
/**
* 创建默认的 安全管理类
* @return
*/
@Bean("defaultWebSecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//关联Realm
securityManager.setRealm(shiroRealm);
securityManager.setSubjectDAO(subjectDAO());
securityManager.setSessionManager(sessionManager());
SecurityUtils.setSecurityManager(securityManager);
return securityManager;
}
/**
* 在方法中 注入 securityManager,进行代理控制
*/
@Bean
public MethodInvokingFactoryBean methodInvokingFactoryBean(DefaultWebSecurityManager securityManager) {
MethodInvokingFactoryBean bean = new MethodInvokingFactoryBean();
bean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
bean.setArguments(securityManager);
return bean;
}
/**
* 缓存管理器 使用Ehcache实现
*/
// @Bean
// public CacheManager getCacheShiroManager(EhCacheManagerFactoryBean ehcache) {
// EhCacheManager ehCacheManager = new EhCacheManager();
// ehCacheManager.setCacheManager(ehcache.getObject());
// return ehCacheManager;
// }
/**
* Shiro生命周期处理器:
* 用于在实现了Initializable接口的Shiro bean初始化时调用Initializable接口回调(例如:UserRealm)
* 在实现了Destroyable接口的Shiro bean销毁时调用 Destroyable接口回调(例如:DefaultSecurityManager)
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 启用shrio授权注解拦截方式,AOP式方法级权限检查
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Shiro的过滤器链
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
//获取配置文件设置
String loginUrl=SpringUtils.getEnvironment().getProperty(LoginConstant.LOGIN_URL_KEY);
String successUrl=SpringUtils.getEnvironment().getProperty(LoginConstant.LOGIN_SUCCESS_URL_KEY);
String unauthorizedUrl=SpringUtils.getEnvironment().getProperty(LoginConstant.UNAUTHORIZED_URL_KEY);
String anonUrl=SpringUtils.getEnvironment().getProperty(LoginConstant.LOGIN_INTERCEPTOR_ANON_KEY,LoginConstant.LOGIN_INTERCEPTOR_ANON_DEFAULT);
String[] anonUrls=anonUrl.split(",");
//配置shiro过滤器工厂
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
/**
* 默认的登陆访问url
*/
shiroFilter.setLoginUrl(loginUrl);
/**
* 登陆成功后跳转的url
*/
shiroFilter.setSuccessUrl(successUrl);
/**
* 没有权限跳转的url
*/
shiroFilter.setUnauthorizedUrl(unauthorizedUrl);
//当运行一个Web应用程序时,Shiro将会创建一些有用的默认Filter实例,并自动地在[main]项中将它们置为可用自动地可用的默认的Filter实例是被DefaultFilter枚举类定义的,枚举的名称字段就是可供配置的名称
/**
* anon---------------org.apache.shiro.web.filter.authc.AnonymousFilter 没有参数,表示可以匿名使用。
* authc--------------org.apache.shiro.web.filter.authc.FormAuthenticationFilter 表示需要认证(登录)才能使用,没有参数
* authcBasic---------org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter 没有参数表示httpBasic认证
* logout-------------org.apache.shiro.web.filter.authc.LogoutFilter
* noSessionCreation--org.apache.shiro.web.filter.session.NoSessionCreationFilter
* perms--------------org.apache.shiro.web.filter.authz.PermissionAuthorizationFilter 参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
* port---------------org.apache.shiro.web.filter.authz.PortFilter port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
* rest---------------org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter 根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
* roles--------------org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
* ssl----------------org.apache.shiro.web.filter.authz.SslFilter 没有参数,表示安全的url请求,协议为https
* user---------------org.apache.shiro.web.filter.authz.UserFilter 没有参数表示必须存在用户,当登入操作时不做检查
*/
/**
* 通常可将这些过滤器分为两组
* anon,authc,authcBasic,user是第一组认证过滤器
* perms,port,rest,roles,ssl是第二组授权过滤器
* 注意user和authc不同:当应用开启了rememberMe时,用户下次访问时可以是一个user,但绝不会是authc,因为authc是需要重新认证的
* user表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求,比如rememberMe 说白了,以前的一个用户登录时开启了rememberMe,然后他关闭浏览器,下次再访问时他就是一个user,而不会authc
*
*
* 举几个例子
* /admin=authc,roles[admin] 表示用户必需已通过认证,并拥有admin角色才可以正常发起'/admin'请求
* /edit=authc,perms[admin:edit] 表示用户必需已通过认证,并拥有admin:edit权限才可以正常发起'/edit'请求
* /home=user 表示用户不一定需要已经通过认证,只需要曾经被Shiro记住过登录状态就可以正常发起'/home'请求
*/
/**
* 覆盖默认的user拦截器
*/
HashMap<String, Filter> myFilters = new HashMap<>();
myFilters.put("user", new ShiroJwtFilter());
shiroFilter.setFilters(myFilters);
/**
* 各默认过滤器常用如下(注意URL Pattern里用到的是两颗星,这样才能实现任意层次的全匹配)
* /admins/**=anon 无参,表示可匿名使用,可以理解为匿名用户或游客
* /admins/user/**=authc 无参,表示需认证才能使用
* /admins/user/**=authcBasic 无参,表示httpBasic认证
* /admins/user/**=ssl 无参,表示安全的URL请求,协议为https
* /admins/user/**=perms[user:add:*] 参数可写多个,多参时必须加上引号,且参数之间用逗号分割,如/admins/user/**=perms["user:add:*,user:modify:*"]。当有多个参数时必须每个参数都通过才算通过,相当于isPermitedAll()方法
* /admins/user/**=port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString。其中schmal是协议http或https等,serverName是你访问的Host,8081是Port端口,queryString是你访问的URL里的?后面的参数
* /admins/user/**=rest[user] 根据请求的方法,相当于/admins/user/**=perms[user:method],其中method为post,get,delete等
* /admins/user/**=roles[admin] 参数可写多个,多个时必须加上引号,且参数之间用逗号分割,如:/admins/user/**=roles["admin,guest"]。当有多个参数时必须每个参数都通过才算通过,相当于hasAllRoles()方法
*
*/
//Shiro验证URL时,URL匹配成功便不再继续匹配查找(所以要注意配置文件中的URL顺序,尤其在使用通配符时)
//配置过滤链
Map<String, String> hashMap = new LinkedHashMap<>();
for (String nonePermissionRe : anonUrls) {
hashMap.put(nonePermissionRe, "anon");
}
hashMap.put("/**", "user");
shiroFilter.setFilterChainDefinitionMap(hashMap);
return shiroFilter;
}
//解决UnavailableSecurityManagerException
@Bean
public FilterRegistrationBean delegatingFilterProxy(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
/**
* 创建Realm
* @return
*/
@Bean("shiroRealm")
public ShiroRealm getShiroRealm(){
return new ShiroRealm();
}
//自动创建代理类
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
daap.setProxyTargetClass(true);
return daap;
}
}
比较重要的代码一是禁用session,二是过滤器的设置
尝试了几种禁用session的方式,感觉这种比较合适些
/**
* 禁用session功能
* @return
*/
@Bean
public DefaultSessionManager sessionManager() {
DefaultSessionManager manager = new DefaultSessionManager();
manager.setSessionValidationSchedulerEnabled(false);
return manager;
}
@Bean
public SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
@Bean
public DefaultSubjectDAO subjectDAO() {
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
return defaultSubjectDAO;
}
在ShiroConfig一般只配置一些通用的过滤约束,具体约束还是要通过注解设置.
处于多模块共用的考虑,过滤的url是从配置文件中获取的.类头的DependsOn注解是为此添加的.
//要求首先把上下文支持组件注册到spring
@DependsOn("springUtils")
- shiroRealm 处理认证与授权逻辑
import com.f4Blog.auth.jwt.ShiroJwtToken;
import com.f4Blog.auth.jwt.JwtUtil;
import com.f4Blog.auth.service.imip.UserAuthServiceImpl;
import com.f4Blog.auth.service.inter.UserAuthService;
import com.f4Blog.basic.util.ToolUtil;
import com.f4Blog.model.base.BaseUser;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.CredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
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 java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 自定义realm
*/
public class ShiroRealm extends AuthorizingRealm {
/**
* 使用jwt代替原生token
*
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof ShiroJwtToken;
}
/**
* 执行授权逻辑
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权逻辑");
return null;
}
/**
* 执行登录认证逻辑
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证逻辑");
UserAuthService shiroFactory = UserAuthServiceImpl.me();
String token = (String)authenticationToken.getCredentials();
String username = JwtUtil.getUsername(token);
BaseUser user = shiroFactory.user(username);
ShiroUser shiroUser = shiroFactory.shiroUser(user);
if(user == null) {
throw new UnknownAccountException("账号不存在");
}
if(!JwtUtil.verify(token, user.getUsername(), user.getPassword())) {
throw new CredentialsException("用户名/密码错误");
}
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(shiroUser, token,getName());
return simpleAuthenticationInfo;
}
}
认证的流程是
1.拿到传入的token
2.获取token的username,从数据库拿到用户对象
3.token与用户对象对比,验证token正确性
4.返回shiro授权对象.
需要注意的是,是不需要SimpleAuthenticationInfo进行认证效验的,效验逻辑是通过
if(user == null) {
throw new UnknownAccountException("账号不存在");
}
if(!JwtUtil.verify(token, user.getUsername(), user.getPassword())) {
throw new CredentialsException("用户名/密码错误");
}
实现的
网上查到的资料,一般都是返回的
new SimpleAuthenticationInfo(token, token,getName());
其目的是让shiro效验恒通过,但是会有一个问题,在其他地方去getCredentials()只能拿到token,拿不到当前登录对信息.
自己做了些修改, 第一个参数设置为创建出的shiroUser 登录对象.
采取jwt的验证,supports方法是必须重写的
/**
* 使用jwt代替原生token
*
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof ShiroJwtToken;
}
ShiroJwtToken 代替shiro的token
import org.apache.shiro.authc.AuthenticationToken;
public class ShiroJwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
// 密钥
private String token;
public ShiroJwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
JwtUtil 工具类,提供一些常用的token的操作方法
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
public class JwtUtil {
//短 过期时间60分钟
private static final long SHORT_EXPIRE_TIME = 1000 * 60 * 60;
//长 过期时间7天
private static final long LONG_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7;
//刷新token的时间节点,token有效时间低于该时间将刷新token 15分钟
private static final long REFRESH_COUNT_DOWN = 1000 * 60 * 15;
/**
* 校验token是否正确
*
* @param token 密钥
* @param username 登录名
* @param password 密码
* @return
*/
public static boolean verify(String token, String username, String password) {
try {
Algorithm algorithm = Algorithm.HMAC256(password);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 获取用户名
*
* @param credentials
* @return
*/
public static String getUsername(String credentials) {
try {
DecodedJWT jwt = JWT.decode(credentials);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 生成签名
*
* @param username
* @param password
* @return
*/
public static String sign(String username, String password) {
// 指定过期时间
Date date = new Date(System.currentTimeMillis() + SHORT_EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(password);
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
}
/**
* token的剩余有效期,单位毫秒
* @param token
* @return
*/
public static Long getRemainingTime(String token) {
try{
DecodedJWT jwt = JWT.decode(token);
Long remainingTime=jwt.getExpiresAt().getTime()-System.currentTimeMillis();
return remainingTime;
}catch (Exception e){
return -1l;
}
}
/**
* 是否应刷新tonken
* @param token
* @return
*/
public static Boolean isRefreshToken(String token){
Long remainingTime=getRemainingTime(token);
return null!=remainingTime && remainingTime>0 && remainingTime<=REFRESH_COUNT_DOWN;
}
/**
* 在cookie和相应头设置token
* @param response
* @param token
*/
public static void cookieAndHeaderSetToken(HttpServletResponse response,String token){
Cookie cookie = new Cookie("token",token);
cookie.setPath( "/");
response.addCookie(cookie);
response.setHeader("token",token);
}
/**
* 在cookie和相应头 获取 token
* @param httpServletRequest
*/
public static String cookieAndHeaderGetToken(HttpServletRequest httpServletRequest){
//获取请求头的token
String token = httpServletRequest.getHeader("token");
if(!StringUtils.hasText(token)){
//请求头没有,尝试从cookie获取token
Cookie[] cookies=httpServletRequest.getCookies();
if(null!=cookies&&cookies.length!=0){
i:for(Cookie cookie:cookies){
if(cookie.getName().equalsIgnoreCase("token")){
token=cookie.getValue();
break i;
}
}
}
}
return token;
}
}
ShiroUser 实体类,用于记录当前登录对象信息
import java.io.Serializable;
import java.util.List;
public class ShiroUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户主键ID
*/
private Long id;
/**
* 账号
*/
private String username;
/**
* 角色id 集合
*/
private List<Long> roleList;
/**
* 角色名称 集合
*/
private List<String> roleNames;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public List<Long> getRoleList() {
return roleList;
}
public void setRoleList(List<Long> roleList) {
this.roleList = roleList;
}
public List<String> getRoleNames() {
return roleNames;
}
public void setRoleNames(List<String> roleNames) {
this.roleNames = roleNames;
}
}
ShiroJwtFilter 过滤器,用于进行token的验证
import com.f4Blog.auth.jwt.JwtUtil;
import com.f4Blog.auth.jwt.ShiroJwtToken;
import com.f4Blog.auth.service.imip.UserAuthServiceImpl;
import com.f4Blog.auth.service.inter.UserAuthService;
import com.f4Blog.basic.reqres.utils.WebUtils;
import com.f4Blog.model.base.BaseUser;
import org.apache.shiro.ShiroException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 进行用户的访问过滤
* 通过继承AccessControlFilter的进行实现
* https://www.cnblogs.com/CESC4/p/7599927.html
* 抽象方法功能:
* isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;
* onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。
*
* 父类提供的方法:
* void setLoginUrl(String loginUrl) //身份验证时使用,默认/login.jsp
* String getLoginUrl()
* Subject getSubject(ServletRequest request, ServletResponse response) //获取Subject实例
* boolean isLoginRequest(ServletRequest request, ServletResponse response)//当前请求是否是登录请求
* void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //将当前请求保存起来并重定向到登录页面
* void saveRequest(ServletRequest request) //将请求保存起来,如登录成功后再重定向回该请求
* void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登录页面
*/
public class ShiroJwtFilter extends AccessControlFilter {
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
if (isLoginRequest(request, response)) {
return true;
} else {
//获取请求头的token
String token = JwtUtil.cookieAndHeaderGetToken(httpServletRequest);
Subject subject = getSubject(request, response);
if(StringUtils.hasText(token)){
//使用token进行shiro认证
try {
subject.login(new ShiroJwtToken(token));
}catch (ShiroException e){
httpServletRequest.setAttribute("tips", "认证失效");
return false;
}
//判断是否应该刷新token
if(JwtUtil.isRefreshToken(token)){
try {
UserAuthService shiroFactory = UserAuthServiceImpl.me();
String username = JwtUtil.getUsername(token);
BaseUser user = shiroFactory.user(username);
String newToken = JwtUtil.sign(user.getUsername(), user.getPassword());
JwtUtil.cookieAndHeaderSetToken(WebUtils.getResponse(),newToken);
}catch (Exception e){
e.printStackTrace();
//日志 刷新token出错
}
}else{
//不刷新,将token重新放回响应头
httpServletResponse.setHeader("token",token);
}
}else{
return false;
}
return true;
}
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
/**
* 如果是ajax请求则不进行跳转
*/
if (httpServletRequest.getHeader("x-requested-with") != null
&& httpServletRequest.getHeader("x-requested-with").equalsIgnoreCase("XMLHttpRequest")) {
return false;
} else {
httpServletResponse.sendRedirect(getLoginUrl());
// saveRequestAndRedirectToLogin(request, response);
}
return false;
}
}
过滤器主要做的事是拿token进行shiro登录,生成用户信息和刷新token.
我了解到的刷新token的方式有三种
1.token时效极短,每次请求都会重新生成token,下次请求需要拿着新token才能通过认证
2.在token即将失效时,刷新token
3.添加一个refresh_token,时效较长,来声明刷新token的权限,对于快失效或者已经失效的token,会效验refresh_token然后判断是否刷新token.
第一种安全性比较高,但是性能开销大.第三种比较繁琐没什么必要,我暂时采取的是第二种方式.
UserAuthServiceImpl 为shiro提供数据库操作
@Service
//要求首先把上下文支持组件注册到spring
@DependsOn("springUtils")
@Transactional(readOnly = true)
public class UserAuthServiceImpl implements UserAuthService {
@Autowired
private RoleDao roleDao;
@Autowired
private UserService userService;
/**
* 获取自身实例对象
* @return
*/
public static UserAuthService me() {
return SpringUtils.getBean(UserAuthService.class);
}
@Override
public BaseUser user(String account) {
BaseUser user = userService.getByUsername(account);
// 账号不存在
if (null == user) {
throw new CredentialsException();
}
// // 账号被冻结
// if (!user.getStatus().equals(ManagerStatus.OK.getCode())) {
// throw new LockedAccountException();
// }
return user;
}
@Override
public ShiroUser shiroUser(BaseUser user) {
ShiroUser shiroUser = createShiroUser(user);
//用户的角色id集合
List<Long> roleIds = roleDao.getRoleIdByUserId(user.getId());
//用户的角色名称集合
List<String> roleNames = roleDao.getRoleNameByUserId(user.getId());
shiroUser.setRoleList(roleIds);
shiroUser.setRoleNames(roleNames);
return shiroUser;
}
@Override
public SimpleAuthenticationInfo info(ShiroUser shiroUser, BaseUser user, String realmName) {
// 密码加盐处理
String salt = user.getSalt();
String credentials = user.getPassword();
ByteSource credentialsSalt = new Md5Hash(salt);
return new SimpleAuthenticationInfo(shiroUser, credentials, credentialsSalt, realmName);
}
/**
* 通过用户表的信息创建一个shiroUser对象
*/
public static ShiroUser createShiroUser(BaseUser user) {
ShiroUser shiroUser = new ShiroUser();
if (user == null) {
return shiroUser;
}
shiroUser.setId(user.getId());
shiroUser.setUsername(user.getUsername());
return shiroUser;
}
}
登录controller相关方法
@RequestMapping(value ="/login",method = RequestMethod.GET)
public String login(ModelAndView model) {
String token=JwtUtil.cookieAndHeaderGetToken(WebUtils.getRequest());
if(StringUtils.hasText(token)){
try{
SecurityUtils.getSubject().login(new ShiroJwtToken(token));
return REDIRECT+"pages/test/index";
} catch (ShiroException e){
}
}
return "/login";
}
@RequestMapping(value ="/login",method = RequestMethod.POST)
public String loginVail(Model model){
//验证是否已登录
String username= WebUtils.get("username");
String password=WebUtils.get("password");
BaseUser user = userService.getByUsername(username);
String md5_password=ShiroKit.md5(password ,user.getSalt());
String token = JwtUtil.sign(user.getUsername(), md5_password);
SecurityUtils.getSubject().login(new ShiroJwtToken(token));
JwtUtil.cookieAndHeaderSetToken(WebUtils.getResponse(),token);
model.addAttribute("token",token);
//登录成功,记录登录日志
return REDIRECT+"pages/test/index";
}
//退出
@RequestMapping("/logout")
public String logout() {
// SecurityUtils.getSubject().logout();
// 移除Cookie中的token
Cookie cookie = new Cookie("token","");
cookie.setValue(null);
cookie.setMaxAge(0);
cookie.setPath( "/");
WebUtils.getResponse().addCookie(cookie);
return REDIRECT+"/login";
}
一些辅助类
GlobalExceptionHandler 全局异常捕获
/**
* 全局的的异常拦截器(拦截所有的控制器)(带有@RequestMapping注解的方法上都会拦截)
*
*/
@ControllerAdvice
/**
*ControllerAdvice注解 用于对Controller进行“切面”环绕
* 其用法主要有三点:
* 结合方法型注解@ExceptionHandler,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的;
* 结合方法型注解@InitBinder,用于request中自定义参数解析方式进行注册,从而达到自定义指定格式参数的目的;
* 结合方法型注解@ModelAttribute,表示其标注的方法将会在目标Controller方法执行之前执行。
*/
@Order(-1)
/**
*注解@Order的作用是定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序
* 默认是最低优先级,值越小优先级越高
*/
public class GlobalExceptionHandler {
private Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 用户未登录异常
*/
@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String unAuth(AuthenticationException e) {
log.error("用户未登陆:", e);
return "/login";
}
/**
* 账号被冻结异常
*/
@ExceptionHandler(DisabledAccountException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String accountLocked(DisabledAccountException e, Model model) {
String username = getRequest().getParameter("username");
// LogManager.me().executeLog(LogTaskFactory.loginLog(username, "账号被冻结", getIp()));
model.addAttribute("tips", "账号被冻结");
return "/login";
}
/**
* 账号密码错误异常
*/
@ExceptionHandler(CredentialsException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String credentials(CredentialsException e, Model model) {
// String username = getRequest().getParameter("username");
// LogManager.me().executeLog(LogTaskFactory.loginLog(username, "账号密码错误", getIp()));
model.addAttribute("tips", "账号密码错误");
return "/login";
}
/**
* 验证码错误异常
*/
@ExceptionHandler(InvalidKaptchaException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String credentials(InvalidKaptchaException e, Model model) {
String username = getRequest().getParameter("username");
// LogManager.me().executeLog(LogTaskFactory.loginLog(username, "验证码错误", getIp()));
model.addAttribute("tips", "验证码错误");
return "/login";
}
/**
* 无权访问该资源异常
*/
@ExceptionHandler(UndeclaredThrowableException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ResponseBody
public ErrorResponseData credentials(UndeclaredThrowableException e) {
getRequest().setAttribute("tip", "权限异常");
log.error("权限异常!", e);
return new ErrorResponseData(BaseExceptionEnum.NO_PERMITION.getCode(), BaseExceptionEnum.NO_PERMITION.getMessage());
}
/**
* 捕获数据绑定异常
* @param ex
* @return
*/
@ExceptionHandler(value={BindException.class, MethodArgumentNotValidException.class})
@ResponseBody
public ErrorResponseData bindExceptionHandler(Exception ex) {
ErrorResponseData res=new ErrorResponseData();
//出现参数不正确的异常,在返回值显示提示信息,具体错误消息进行记录和打印
//设置为参数错误
res.addEnumInfo(BaseExceptionEnum.PARA_ERROR);
List<FieldError> fieldErrors=null;
if(ex instanceof BindException){
fieldErrors=((BindException)ex).getBindingResult().getFieldErrors();
}else if(ex instanceof MethodArgumentNotValidException){
fieldErrors=((MethodArgumentNotValidException)ex).getBindingResult().getFieldErrors();
}else{
return res;
}
List<String> allError=new ArrayList<>();
//将第一个错误信息作为响应的错误信息
res.setMessage(fieldErrors.get(0).getDefaultMessage());
//记录其他错误信息
for (FieldError error:fieldErrors){
allError.add(error.getDefaultMessage());
}
res.setData(allError);
return res;
}
/**
* 捕获消息型的异常
* @param ex
* @return
*/
@ExceptionHandler(value={ServiceException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ErrorResponseData exceptionHandler(ServiceException ex) {
ErrorResponseData res=new ErrorResponseData();
res.addExceptionInfo(ex);
log.error("业务异常:", ex);
return res;
}
/**
* 捕获出错型的异常
* @param ex
* @return
*/
@ExceptionHandler(value={ErrorException.class})
@ResponseBody
public ErrorResponseData exceptionHandler(ErrorException ex) {
ErrorResponseData res=new ErrorResponseData();
//出现错误型异常,在返回值显示服务器异常,具体错误消息进行记录和打印
ex.printStackTrace();
log.error("系统执行出现异常:" + ex.getMessage());
return res;
}
/**
* 捕获其他异常
* @param ex
* @return
*/
@ExceptionHandler(value={Exception.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ErrorResponseData exceptionHandler(Exception ex) {
ErrorResponseData res=new ErrorResponseData();
//不确定的异常,在返回值显示未知异常,具体错误消息进行记录和打印
res.addEnumInfo(BaseExceptionEnum.UNKNOWN_ERROR);
ex.printStackTrace();
this.log.error( "系统出现未知异常 :" + ex.getMessage());
return res;
}
}
3.后续需要考虑修改地方
1.token验证增加缓存
2.前端增加全局的设置token到header
3.实现一个类似[7天内免登录]的效果
4.增加错误页面