至此,shiro和springboot和JWT的集成全部完毕,下面为完整代码。
第一:shiroConfig,整个shiro的配置类。
这里sesssion交给了shiro默认去管理,不在做过多的配置,一方面shiro的session时间够用了,另一方便,没必要把配置类搞得这么庞大,以前的shiro文章里特地讲了shiro的session,包括集成quatz。请看这里
/** * shiro配置类 */ @Configuration public class ShiroConfig { @Bean("rememberCookie") public SimpleCookie rememberCookie(){ SimpleCookie simpleCookie = new SimpleCookie(); simpleCookie.setHttpOnly(true); simpleCookie.setName("remeberCookie"); simpleCookie.setMaxAge(360000); return simpleCookie; } @Bean("cookieRememberMe") public CookieRememberMeManager cookieRememberMe(){ CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberCookie()); return cookieRememberMeManager; } /** * 密码加密 * @return */ @Bean("credentialsMatcher") public HashedCredentialsMatcher credentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("SHA-256"); hashedCredentialsMatcher.setHashIterations(20); hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); return hashedCredentialsMatcher; } @Bean("shiroRealm") public ShiroRealm shiroRealm(){ ShiroRealm shiroRealm = new ShiroRealm(); shiroRealm.setCredentialsMatcher(credentialsMatcher()); return shiroRealm; } /** * 这里session交给shiro默认管理,不去做详细配置 * @return */ @Bean("securityManager") public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //传入自定义shiroRealm securityManager.setRealm(shiroRealm()); //这里要注意,setRememberMeManager里传入的类型是CookieRememberMeManager,不要搞错了 securityManager.setRememberMeManager(cookieRememberMe()); return securityManager; } /** * 这里不设置loginUrl,传统的前后端不分离是可以配置, * 因为这里前后端分离,前端没有获取到token自然会路由到登录页面进行登录操作,不再需要通过loginUrl定位到视图view,SpringBoot只负责后端 * 同时注意这里的filterMap中对于拦截路径的配置要以 / 开头,否则找不到对应的Controller, * 切记:一定要把/** = authc放到最后!!!!! * 2019-05-12补充说明:对于loginUrl最终还是需要的,filter里会进行是否为登录url的判断。 * @param securityManager * @return */ @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,AuthFilter authFilter){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/user/login"); //设置自定义filter Map<String, Filter> filter = new HashMap<>(); filter.put("auth",authFilter); shiroFilterFactoryBean.setFilters(filter); //同时这里注意,使用LinkedHashMap来保证拦截器的顺序性 Map<String,String> filterMap = new LinkedHashMap<>(); //登录逻辑这里不用anno,统一走自定义filter //filterMap.put("/user/login","anon"); filterMap.put("/user/insert","auth"); filterMap.put("/**","auth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); return shiroFilterFactoryBean; } /** * Spring管理Shiro生命周期 * @return */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){ return new LifecycleBeanPostProcessor(); } }第二:shiroRealm,承担着shiro的认证和授权工作。
/** * 自定义shiroRealm */ public class ShiroRealm extends AuthorizingRealm { @Autowired UserService service; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { principalCollection.getPrimaryPrincipal(); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); return simpleAuthorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); User user = 对应数据库查询操作,可根据自己的代码书写即可; SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),getName()); return simpleAuthenticationInfo; } }第三:AuthFilter.shiro所有请求必须要走的过滤器
/** * shiro过滤器 */ @Component("authFilter") public class AuthFilter extends FormAuthenticationFilter { @Autowired JwtUtil jwtUtil; /** * 判断token是否为空、过期 * * @param request * @param response * @param mappedValue * @return */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { String token = getRequestToken((HttpServletRequest) request); if (ObjectUtils.isNull(token)){ return false; } if (StringUtils.isBlank(token)) { throw new CustomException(jwtUtil.getHeader()+"不能为空", HttpStatus.SC_UNAUTHORIZED); } Claims claims = jwtUtil.parseToken(token); if (ObjectUtils.isNull(claims) || jwtUtil.isTokenExpired(claims.getExpiration())) { throw new CustomException(jwtUtil.getHeader()+"token过期",HttpStatus.SC_UNAUTHORIZED); } return true; } /** * 上面的方法如果返回false,则接下来会执行这个方法,如果返回为true,则不会执行这个方法 * 判断是否为登录url,进一步判断请求是不是post * * @param request * @param response * @return * @throws Exception */ @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { if (isLoginRequest(request, response)) { if (isLoginSubmission(request, response)) { return true; } } return false; } /** * 获取请求中的token,首先从请求头中获取,如果没有,则尝试从请求参数中获取 * * @param request * @return */ private String getRequestToken(HttpServletRequest request) { String token = request.getHeader(jwtUtil.getHeader()); if (StringUtils.isBlank(token)) { token = request.getParameter(jwtUtil.getHeader()); } return token; } }第四:JwtUtil.负责JWT的创建和校验。
/** * JWT token 工具类,提供JWT生成,校验,工作 */ @ConfigurationProperties(prefix = "dhb.jwt") @Component public class JwtUtil { private Logger logger = LoggerFactory.getLogger(getClass()); private String secret; private Long expire; private String header; /** * * 生成JWT token * @param userId * @return */ public String generateToken(Long userId) { Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + expire * 1000); return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(userId + "") .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } /** * * 解析JWT token * @param token * @return */ public Claims parseToken(String token) { try { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { logger.info("解析token出错"); return null; } } /** * * 校验token是否过期 * @param expiprationTime * @return */ public boolean isTokenExpired(Date expiprationTime){ return expiprationTime.before(new Date()); } public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public Long getExpire() { return expire; } public void setExpire(Long expire) { this.expire = expire; } public String getHeader() { return header; } public void setHeader(String header) { this.header = header; } }写在最后:虽然看起来这有这四大步,但是回头看看自己花了好长时间才走到现在这样。从一开始的认识shiro的架构,简单的基本使用,到实际运用中踩的坑,以及为什么要用token,不用session,以及舍弃配置复杂的session和quartz集成管理。这期间,每一步自己都在想,我为什么要用某一个技术,在实际编码中如何做到不让最后集成起来很臃肿。
推荐对于平时自己测试接口的时候,使用PostMan或者Yapi,千万不要为了测一个小小的接口又去写页面之类的,这样会使你脱离工作重心,最后啥啥都没干好。
还是那句话:既然要做,就做的细致一点,对得起自己!