JSON Web Token(JWT)是为了在网络应用间传递声明而执行的一种基于JSON的开放标准。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。token可以直接被用于认证,也可被加密。
我们在springboot+shiro的基础上,整合jwt模块,对其进行扩展,实现无状态认证加鉴权。
JWT实现认证思路:
因为要实现无状态,所以jwt需要关闭shiro的session管理
用户第一次登录成功时,使用jwt返回token
在后续的请求中携带token,每次请求都会对token携带的用户信息进行验证,并完成后续认证和鉴权
1、导入Maven依赖:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.7.0</version> </dependency>
2、封装JwtToken来替换shiro的原生token,需要实现AuthenticationToken接口:
public class JwtToken implements AuthenticationToken { private final String token; public JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
3、添加一个工具类JwtUtil来操作token:
public class JwtUtil { public static final String ACCOUNT = "username"; public static final long EXPIRE_TIME = 30 * 60 * 1000; public static boolean verify(String token, String username, String secret) { try{ Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim(ACCOUNT, username) .build(); DecodedJWT jwt = verifier.verify(token); return true; }catch (Exception e){ e.printStackTrace(); return false; } } public static String getClaimField(String token,String claim){ try{ DecodedJWT jwt = JWT.decode(token); return jwt.getClaim(claim).asString(); }catch (JWTDecodeException e){ e.printStackTrace(); return null; } } public static String sign(String username, String secret) { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); return JWT.create() .withClaim(ACCOUNT, username) .withExpiresAt(date) .sign(algorithm); } }
verify方法中,根据密码生成jwt校验器,校验token是否正确
getClaimField方法用于获得token中指定字段的信息
sign方法用于生成附带用户信息及过期时间的签名
4、添加JwtFilter 拦截器,继承AccessControlFilter 类,验证从请求的header中取出的token信息:
public class JwtFilter extends AccessControlFilter { public static String ACCESS_TOKEN = "Access-Token"; @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest req = (HttpServletRequest) request; // 解决跨域问题 if(HttpMethod.OPTIONS.toString().matches(req.getMethod())) { return true; } if (isLoginAttempt(request, response)) { JwtToken token = new JwtToken(req.getHeader(ACCESS_TOKEN)); try { getSubject(request, response).login(token); return true; } catch (Exception e) { } } onLoginFail(response); return false; } protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; String authorization = req.getHeader(ACCESS_TOKEN); return authorization != null; } //登录失败时默认返回401状态码 private void onLoginFail(ServletResponse response) throws IOException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.getWriter().write("login fail"); } }
首先调用isAccessAllowed,返回false,所有权限认证统一调用onAccessDenied方法。
5、创建JwtRealm 继承AuthorizingRealm 类,Realm中实现了shiro认证的主要功能,包括认证和鉴权两个方面:
public class JwtRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = principals.toString(); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRoles(userService.getRoles(username)); simpleAuthorizationInfo.addStringPermissions(userService.getPermissions(username)); return simpleAuthorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String token = (String) authenticationToken.getCredentials(); String userName = null; try { userName = JwtUtil.getClaimField(token, JwtUtil.ACCOUNT); User user = userService.getUserByName(userName); if (user == null) { System.out.println("用户不存在"); return null; } boolean verify = JwtUtil.verify(token, userName, user.getPassword()); if (!verify) { System.out.println("Token校验不正确"); return null; } } catch (Exception e) { e.printStackTrace(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( userName,token,getName()); return authenticationInfo; } }
6、创建JwtSubjectFactory,关闭session:
public class JwtSubjectFactory extends DefaultWebSubjectFactory { @Override public Subject createSubject(SubjectContext context) { context.setSessionCreationEnabled(false); return super.createSubject(context); } }
7、创建ShiroConfig进行配置,这里除了配置shiro自身三个核心组件filter、realm、securityManager的注入外,还关闭了shiro的会话管理,注入Subject工厂,以及开启对shiro注解的支持:
@Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilter(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager webSecurityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(webSecurityManager); Map<String,String> filterChainDefinitionMap=new LinkedHashMap<>(); filterChainDefinitionMap.put("/toLogin","anon"); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("/index"); //shiro自定义过滤器 Map<String, Filter> filters = new LinkedHashMap<>(); filters.put("jwt", new JwtFilter()); shiroFilterFactoryBean.setFilters(filters); filterChainDefinitionMap.put("/**","jwt"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager defaultSessionManager = new DefaultWebSessionManager(); defaultSessionManager.setSessionValidationSchedulerEnabled(false); return defaultSessionManager; } @Bean public DefaultWebSubjectFactory subjectFactory() { return new JwtSubjectFactory(); } @Bean(name = "defaultWebSecurityManager") public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("realm") JwtRealm realm, SubjectFactory subjectFactory, SessionManager sessionManager){ DefaultWebSecurityManager webSecurityManager=new DefaultWebSecurityManager(); webSecurityManager.setRealm(realm); //关闭shiro自带的session DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); webSecurityManager.setSubjectDAO(subjectDAO); webSecurityManager.setSubjectFactory(subjectFactory); webSecurityManager.setSessionManager(sessionManager); return webSecurityManager; } @Bean(name = "realm") public JwtRealm myRealm(){ return new JwtRealm(); } @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
需要注意,不要通过注解的方式直接把自定义的JwtFilter注入到spring容器中,而是在ShiroFilter中注入JwtFilter并使用它拦截一切非匿名访问的请求。
8、实现登录逻辑,验证密码是否正确,登录成功后返回签发的token:
@Controller public class LoginController { @Autowired UserService userService; @ResponseBody @PostMapping(value = "toLogin") public Result<JSONObject> login(String username, String password) { Result<JSONObject> result = new Result<>(); JSONObject json = new JSONObject(); User user = userService.getUserByName(username); if (user == null) { json.put("error", "用户不存在"); result.setData(json); return result; } if (!user.getPassword().equals(password)) { json.put("error", "密码不正确"); result.setData(json); return result; } String token = JwtUtil.sign(username, password); json.put("token", token); result.setData(json); return result; } }
9、定义测试接口,并定义接口访问权限:
@RestController public class TestController { @GetMapping("test1") @RequiresRoles("admin") @RequiresPermissions("user:add") public String test(){ return "test1"; } @GetMapping("test2") @RequiresPermissions("other:copy") public String test2(){ return "test2"; } @GetMapping("test3") @RequiresRoles("guest") @RequiresPermissions("other:check") public String test3(){ return "test3"; } }
10、使用postman进行测试,登录成功后会返回token:
调用接口时携带刚才返回的token,调用成功并返回结果:
访问没有权限的接口,shiro会对其进行拦截: