spring-boot集成8:集成shiro,jwt
Shrio是一个轻量级的,基于AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。
JWT(JSON Web Token)是目前最流行的跨域身份验证解决方案,具有加密和自包含的特性。
1.maven配置
<!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <!--jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.2.0</version> </dependency>
2.自定义Token Authentication
package com.bby.security; import org.apache.shiro.authc.AuthenticationToken; /** * 自定义jwt类型的token * * @author: zhangyang * @create: 2018/11/28 8:39 **/ public class MyJWTToken implements AuthenticationToken { private String token; public MyJWTToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
3.自定义Shiro过滤器
package com.bby.security; import com.alibaba.fastjson.JSONObject; import com.bby.common.vo.Result; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.util.AntPathMatcher; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.PrintWriter; /** * jwt token filter * * @Author zhangyang * @Date 下午 8:42 2018/11/27 0027 **/ @Slf4j public class MyJWTFilter extends BasicHttpAuthenticationFilter { private String tokenHeader; private String loginUri; public MyJWTFilter(String tokenHeader, String loginUri) { this.tokenHeader = tokenHeader; this.loginUri = loginUri; } /** * 如果是登录则直接放行; * 如果带有 token,则对 token 进行检查 * * @param request * @param response * @param mappedValue * @return */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { HttpServletRequest req = (HttpServletRequest) request; AntPathMatcher matcher = new AntPathMatcher(); // 开放登录接口访问 if (matcher.match(loginUri, req.getRequestURI())) { return true; } // 判断请求的请求头是否带上token if (StringUtils.isBlank(req.getHeader(tokenHeader))) { return false; } // 如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确 try { executeLogin(request, response); } catch (Exception e) { log.error(e.getMessage()); try { // globalExceptionHandler无法处理filter中的异常,这里手动处理 PrintWriter out = response.getWriter(); out.print(JSONObject.toJSON(Result.failureWithCode(Result.UNAUTHORIZED, e.getMessage()))); out.flush(); } catch (IOException e1) { e1.printStackTrace(); } return false; } return true; } /** * 执行登陆操作 * * @param request * @param response * @return */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader(tokenHeader); MyJWTToken jwtToken = new MyJWTToken(token); // 提交给realm进行登入,如果错误他会抛出异常并被捕获 getSubject(request, response).login(jwtToken); // 如果没有抛出异常则代表登入成功,返回true return true; } }
4.自定义Shiro Realm
package com.bby.security; import com.bby.common.util.RedisRepository; import com.bby.mapper.system.SysUserMapper; import org.apache.commons.collections4.CollectionUtils; 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; import java.util.List; /** * 实现AuthorizingRealm接口用户用户认证 * * @author: zhangyang * @create: 2018/11/24 21:25 **/ @Component public class MyRealm extends AuthorizingRealm { @Autowired private RedisRepository redisRepository; @Autowired private SysUserMapper sysUserMapper; /** * 获取用户权限信息 * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 获取token String token = (String) principalCollection.getPrimaryPrincipal(); // 查询用户权限信息 List<String> permissions = sysUserMapper.getPermissionByUserId(JWTUtil.getUserId(token)); // 只添加权限(角色方式不灵活) SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); if (CollectionUtils.isNotEmpty(permissions)) { simpleAuthorizationInfo.addStringPermissions(permissions); } return simpleAuthorizationInfo; } /** * 获取用户认证信息 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 加这一步的目的是在Post请求的时候会先进认证,然后在到请求 Object principal = authenticationToken.getPrincipal(); if (principal == null) { return null; } String token = (String) principal; // 解密获得username,用于和数据库进行对比 String claim = JWTUtil.getUserId(token); // 验证缓存中的登录状态 if (claim == null || !JWTUtil.verify(token, claim) || !redisRepository.exists(token)) { throw new AuthenticationException("token validation failed"); } // 获取用户信息 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token, token, getName()); return simpleAuthenticationInfo; } /** * 设置支持的token类型为自定义jwtToken * * @param token * @return */ @Override public boolean supports(AuthenticationToken token) { return token instanceof MyJWTToken; } /** * 覆盖验证密码是否匹配的方法,因为在自定义的login方法中已经实现了 * * @param token * @param info * @throws AuthenticationException */ @Override protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { } }
5.登录代码
package com.bby.controller; import com.bby.common.vo.Result; import com.bby.security.JWTUtil; import com.bby.service.system.ISysUserService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.Map; /** * 登录/鉴权controller * * @author: zhangyang * @create: 2018/11/25 20:26 **/ @Api(tags = "认证授权") @RestController @RequestMapping("auth") public class AuthController { @Autowired private ISysUserService sysUserService; /** * 登录 * * @param authInfo * @return */ @ApiOperation("登录") @PostMapping("login") public Result login(@ApiParam("用户名") @RequestBody Map<String, String> authInfo) { return sysUserService.validate(authInfo.get("username"), authInfo.get("password")); } /** * 登出 * * @return */ @ApiOperation("登出") @PostMapping("logout") public Result logout(HttpServletRequest request) { // 因为token的方式是无状态的,要实现登出,则需要使用缓存来保存状态 return sysUserService.logout(JWTUtil.getToken(request)); } /** * 用户信息 * 用户名: * @return */ @ApiOperation("获取用户信息") @GetMapping("info") public Result info(HttpServletRequest request) { return sysUserService.getAuthInfo(JWTUtil.getUserId(JWTUtil.getToken(request))); } }
6.shiro配置
package com.bby.security; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.mgt.SecurityManager; 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.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import javax.servlet.Filter; import java.util.HashMap; import java.util.Map; /** * shiro 配置 * * @author: zhangyang * @create: 2018/11/24 21:34 **/ @Configuration public class ShiroConfiguration { @Value("${jwt.token-header}") private String tokenHeader; @Value("${jwt.filter-name}") private String jwtFilterName; @Value("${login.uri}") private String loginUri; /** * 将自己的验证方式加入容器 * * @return */ @Autowired private MyRealm myRealm; /** * 权限管理,配置主要是Realm的管理认证 * * @return */ @Bean @DependsOn("myRealm") public SecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); // 使用自己的realm System.out.println(myRealm); manager.setRealm(myRealm); // 关闭shiro自带的session DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); manager.setSubjectDAO(subjectDAO); return manager; } /** * Filter工厂,设置对应的过滤条件和跳转条件 * * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); // 添加自己的过滤器并且取名为jwt Map<String, Filter> filterMap = new HashMap<>(); System.out.println(tokenHeader); filterMap.put("jwt", new MyJWTFilter(tokenHeader, loginUri)); factoryBean.setFilters(filterMap); factoryBean.setSecurityManager(securityManager); factoryBean.setUnauthorizedUrl("/401"); Map<String, String> filterRuleMap = new HashMap<>(); // 所有请求通过我们自己的JWT Filter filterRuleMap.put("/**", "jwt"); // 访问401和404页面不通过我们的Filter filterRuleMap.put("/401", "anon"); factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; } /** * 下面的代码是添加注解支持 */ @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); // 强制使用cglib,防止重复代理和可能引起代理出错的问题 // https://zhuanlan.zhihu.com/p/29161098 defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }