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; } }