1.引入shiro、jwt的依赖库
html
org.apache.shiro shiro-web 1.7.1 org.apache.shiro shiro-spring 1.7.1 com.auth0 java-jwt 3.16.0
2.添加自定义认证token类,实现shiro的AuthenticationToken接口java
import org.apache.shiro.authc.AuthenticationToken; public class OAuth2Token implements AuthenticationToken { private static final long serialVersionUID = 451903942673942296L; private String token; public OAuth2Token(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
3.建立realm类,实现认证、受权的接口web
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.stereotype.Component; @Component public class OAuth2Realm extends AuthorizingRealm { @Autowired private JwtUtil jwtUtil; @Override public boolean supports(AuthenticationToken token) { return token instanceof OAuth2Token; } /** * 受权(验证权限时调用) */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // TODO 查询用户的权限列表 // TODO 把权限列表添加到info对象中 return info; } /** * 认证(登陆时调用) */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // TODO 从令牌中获取userId,而后检测该帐户是否被冻结。 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(); // TODO 往info对象中添加用户信息、Token字符串 return info; } }
4.令牌的刷新机制
给令牌设置有效期,并把令牌放到redis中,设置redis中数据的有效期大于令牌的有限期。当校验到令牌过时后,从redis中获取数据,若是能获取到数据,则生成新的令牌对令牌进行续期。
若是redis中没有获取到数据,即redis中的数据也过时了,则用户必须从新登录。redis
5.建立ThreadLocal类,在线程中存储tokenspring
public class ThreadLocalToken { private static ThreadLocallocal = new ThreadLocal<>(); public static void setToken(String token) { local.set(token); } public static String getToken() { return (String) local.get(); } public static void clear() { local.remove(); } }
6.添加AuthenticatingFilter处理类,判断请求是否须要作过滤,以及须要过滤后作令牌的校验和权限的判断apache
import java.io.IOException; import java.util.concurrent.TimeUnit; 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.http.HttpStatus; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestMethod; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.TokenExpiredException; @Component @SuppressWarnings("rawtypes") public class OAuth2Filter extends AuthenticatingFilter { @Value("${emos.jwt.cache-expire}") private int cacheExpire; @Autowired private JwtUtil jwtUtil; @Autowired private RedisTemplate redisTemplate; /** * 拦截请求以后,用于把令牌字符串封装成令牌对象 */ @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { // 获取请求token String token = getRequestToken((HttpServletRequest) request); if (StringUtils.isBlank(token)) { return null; } return new OAuth2Token(token); } /** * 拦截请求,判断请求是否须要被Shiro处理 */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { HttpServletRequest req = (HttpServletRequest) request; // Ajax提交application/json数据的时候,会先发出Options请求 // 这里要放行Options请求,不须要Shiro处理 if (req.getMethod().equals(RequestMethod.OPTIONS.name())) { return true; } // 除了Options请求以外,全部请求都要被Shiro处理 return false; } /** * 该方法用于处理全部应该被Shiro处理的请求 */ @SuppressWarnings("unchecked") @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; resp.setHeader("Content-Type", "text/html;charset=UTF-8"); // 容许跨域请求 resp.setHeader("Access-Control-Allow-Credentials", "true"); resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin")); ThreadLocalToken.clear(); // 获取请求token,若是token不存在,直接返回401 String token = getRequestToken((HttpServletRequest) request); if (StringUtils.isBlank(token)) { resp.setStatus(HttpStatus.SC_UNAUTHORIZED); resp.getWriter().print("无效的令牌"); return false; } try { jwtUtil.verifierToken(token); // 检查令牌是否过时 } catch (TokenExpiredException e) { // 客户端令牌过时,查询Redis中是否存在令牌,若是存在令牌就从新生成一个令牌给客户端 if (redisTemplate.hasKey(token)) { redisTemplate.delete(token);// 删除令牌 int userId = jwtUtil.getUserId(token); token = jwtUtil.createToken(userId); // 生成新的令牌 // 把新的令牌保存到Redis中 redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS); // 把新令牌绑定到线程 ThreadLocalToken.setToken(token); } else { // 若是Redis不存在令牌,让用户从新登陆 resp.setStatus(HttpStatus.SC_UNAUTHORIZED); resp.getWriter().print("令牌已通过期"); return false; } } catch (JWTDecodeException e) { resp.setStatus(HttpStatus.SC_UNAUTHORIZED); resp.getWriter().print("无效的令牌"); return false; } // 登陆和受权校验工做 boolean bool = executeLogin(request, response); return bool; } /** * 登陆失败后的返回信息 */ @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; resp.setStatus(HttpStatus.SC_UNAUTHORIZED); resp.setContentType("application/json;charset=utf-8"); resp.setHeader("Access-Control-Allow-Credentials", "true"); resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin")); try { resp.getWriter().print(e.getMessage()); } catch (IOException exception) { } return false; } /** * 获取请求头里面的token */ private String getRequestToken(HttpServletRequest httpRequest) { // 从header中获取token String token = httpRequest.getHeader("token"); // 若是header中不存在token,则从参数中获取token if (StringUtils.isBlank(token)) { token = httpRequest.getParameter("token"); } return token; } }
7.建立配置类ShiroConfigjson
import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import javax.servlet.Filter; import org.apache.shiro.mgt.SecurityManager; 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.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ShiroConfig { @Bean("securityManager") public SecurityManager securityManager(OAuth2Realm oAuth2Realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(oAuth2Realm); securityManager.setRememberMeManager(null); return securityManager; } @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, OAuth2Filter oAuth2Filter) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); // oauth过滤 Mapfilters = new HashMap<>(); filters.put("oauth2", oAuth2Filter); shiroFilter.setFilters(filters); MapfilterMap = new LinkedHashMap<>(); filterMap.put("/webjars/**", "anon"); filterMap.put("/druid/**", "anon"); filterMap.put("/app/**", "anon"); filterMap.put("/sys/login", "anon"); filterMap.put("/swagger/**", "anon"); filterMap.put("/v2/api-docs", "anon"); filterMap.put("/swagger-ui.html", "anon"); filterMap.put("/swagger-resources/**", "anon"); filterMap.put("/captcha.jpg", "anon"); filterMap.put("/user/register", "anon"); filterMap.put("/user/login", "anon"); filterMap.put("/test/**", "anon"); filterMap.put("/**", "oauth2"); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } @Bean("lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
8.利用AOP把新的token返回给客户端api
import javax.servlet.http.HttpServletResponse; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import com.example.emos.wx.config.shiro.ThreadLocalToken; @Aspect @Component public class TokenAspect { @Pointcut("execution(public * com.example.emos.wx.controller.*.*(..)))") public void aspect() { } @Around("aspect()") public Object around(ProceedingJoinPoint point) throws Throwable { Object result = point.proceed(); // 方法执行结果 String token = ThreadLocalToken.getToken(); // 若是ThreadLocal中存在Token,说明是更新的Token if (token != null) { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); response.setHeader("token", token); // 往响应中放置Token ThreadLocalToken.clear(); } return result; } }