这篇文章我们来讲一下如何集成JWT到Spring Boot项目中来完成接口的权限验证。
JWT JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准( RFC 7519 ),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。 如何使用JWT?
在身份鉴定的实现中,传统方法是在服务端存储一个session,给客户端返回一个cookie,而使用JWT之后,当用户使用它的认证信息登陆系统之后,会返回给用户一个JWT,用户只需要本地保存该token(通常使用local storage,也可以使用cookie)即可。
因为用户的状态在服务端的内存中是不存储的,所以这是一种 无状态 的认证机制。服务端的保护路由将会检查请求头 Authorization 中的JWT信息,如果合法,则允许用户的行为。由于JWT是自包含的,因此减少了需要查询数据库的需要。
JWT的这些特性使得我们可以完全依赖其无状态的特性提供数据API服务,甚至是创建一个下载流服务。因为JWT并不使用Cookie的,所以你可以使用任何域名提供你的API服务而不需要担心跨域资源共享问题(CORS)。 大概就是这样:
Spring Boot集成 我是勤劳的搬运工,这应该是翻译老外的东西 ,项目地址:https://github.com/thomas-kendall/trivia-microservices。
废话不多说了,我直接上代码,依然是搬运工。 我是gradle构建的,就是引入一些依赖的jar包。顺便推荐一下阿里云的中央仓库
http://maven.aliyun.com/nexus/content/groups/public/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 dependencies { compile('org.springframework.boot:spring-boot-starter-aop' ) compile('org.springframework.boot:spring-boot-starter-security' ) compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.1.1' ) compile('org.springframework.boot:spring-boot-starter-web' ) compile('com.google.guava:guava:20.0' ) compile('com.alibaba:druid:0.2.9' ) compile('org.apache.commons:commons-lang3:3.5' ) compile('commons-collections:commons-collections:3.2.2' ) compile('commons-codec:commons-codec:1.10' ) compile('com.github.pagehelper:pagehelper:4.1.6' ) compile('io.jsonwebtoken:jjwt:0.6.0' ) runtime('mysql:mysql-connector-java' ) compileOnly('org.projectlombok:lombok' ) testCompile('org.springframework.boot:spring-boot-starter-test' ) }
下面这个是类是产生token的主要类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 @Slf4j public class JsonWebTokenUtility { private SignatureAlgorithm signatureAlgorithm; private Key secretKey; public JsonWebTokenUtility () { signatureAlgorithm = SignatureAlgorithm.HS512; String encodedKey = "L7A/6zARSkK1j7Vd5SDD9pSSqZlqF7mAhiOgRbgv9Smce6tf4cJnvKOjtKPxNNnWQj+2lQEScm3XIUjhW+YVZg==" ; secretKey = deserializeKey(encodedKey); } public String createJsonWebToken (AuthTokenDetails authTokenDetails) { String token = Jwts.builder().setSubject(authTokenDetails.getId().toString()) .claim("username" , authTokenDetails.getUsername()) .claim("roleNames" , authTokenDetails.getRoleNames()) .setExpiration(authTokenDetails.getExpirationDate()) .signWith(getSignatureAlgorithm(), getSecretKey()).compact(); return token; } private Key deserializeKey (String encodedKey) { byte [] decodedKey = Base64.getDecoder().decode(encodedKey); Key key = new SecretKeySpec(decodedKey, getSignatureAlgorithm().getJcaName()); return key; } private Key getSecretKey () { return secretKey; } public SignatureAlgorithm getSignatureAlgorithm () { return signatureAlgorithm; } public AuthTokenDetails parseAndValidate (String token) { AuthTokenDetails authTokenDetails = null ; try { Claims claims = Jwts.parser().setSigningKey(getSecretKey()).parseClaimsJws(token).getBody(); String userId = claims.getSubject(); String username = (String) claims.get("username" ); List<String> roleNames = (List) claims.get("roleNames" ); Date expirationDate = claims.getExpiration(); authTokenDetails = new AuthTokenDetails(); authTokenDetails.setId(Long.valueOf(userId)); authTokenDetails.setUsername(username); authTokenDetails.setRoleNames(roleNames); authTokenDetails.setExpirationDate(expirationDate); } catch (JwtException ex) { log.error(ex.getMessage(), ex); } return authTokenDetails; } private String serializeKey (Key key) { String encodedKey = Base64.getEncoder().encodeToString(key.getEncoded()); return encodedKey; } }
现在我们需要一个定制授权过滤器,将能读取请求头部信息,在Spring中已经有一个这样的授权Filter称为:RequestHeaderAuthenticationFilter,我们只要扩展继承即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Component public class JsonWebTokenAuthenticationFilter extends RequestHeaderAuthenticationFilter { public JsonWebTokenAuthenticationFilter () { this .setExceptionIfHeaderMissing(false ); this .setPrincipalRequestHeader("Authorization" ); } @Override @Autowired public void setAuthenticationManager ( AuthenticationManager authenticationManager) { super .setAuthenticationManager(authenticationManager); } }
在这里,头部信息将被转换为Spring Authentication对象,名称为PreAuthenticatedAuthenticationToken 我们需要一个授权提供者读取这个记号,然后验证它,然后转换为我们自己的定制授权对象,就是把header里的token转化成我们自己的授权对象。然后把解析之后的对象返回给Spring Security,这里就相当于完成了token->session的转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 @Component public class JsonWebTokenAuthenticationProvider implements AuthenticationProvider { private JsonWebTokenUtility tokenService = new JsonWebTokenUtility(); @Override public Authentication authenticate (Authentication authentication) throws AuthenticationException { Authentication authenticatedUser = null ; if (authentication.getClass(). isAssignableFrom(PreAuthenticatedAuthenticationToken.class) && authentication.getPrincipal() != null ) { String tokenHeader = (String) authentication.getPrincipal(); UserDetails userDetails = parseToken(tokenHeader); if (userDetails != null ) { authenticatedUser = new JsonWebTokenAuthentication(userDetails, tokenHeader); } } else { authenticatedUser = authentication; } return authenticatedUser; } private UserDetails parseToken (String tokenHeader) { UserDetails principal = null ; AuthTokenDetails authTokenDetails = tokenService.parseAndValidate(tokenHeader); if (authTokenDetails != null ) { List<GrantedAuthority> authorities = authTokenDetails.getRoleNames().stream() .map(SimpleGrantedAuthority::new ).collect(Collectors.toList()); principal = new User(authTokenDetails.getId().toString(), "" , authorities); } return principal; } @Override public boolean supports (Class<?> authentication) { return authentication.isAssignableFrom( PreAuthenticatedAuthenticationToken.class)|| authentication.isAssignableFrom( JsonWebTokenAuthentication.class); } }
Spring Security 上面完成了JWT和Spring Boot的集成。 接下来我们再如何把自己的权限系统也接入Spring Security。 刚才已经展示了通过JsonWebTokenAuthenticationProvider的处理,我们已经能通过header的token来识别用户,并拿到他的角色和userId等信息。
配置Spring Security有3个不可缺的类。 首先配置拦截器,拦截所有的请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 @Component public class DemoSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { @Autowired private FilterInvocationSecurityMetadataSource securityMetadataSource; @Autowired @Override public void setAccessDecisionManager (AccessDecisionManager accessDecisionManager) { super .setAccessDecisionManager(accessDecisionManager); } @Autowired @Override public void setAuthenticationManager (AuthenticationManager authenticationManager) { super .setAuthenticationManager(authenticationManager); } public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } public Class<? extends Object> getSecureObjectClass() { return FilterInvocation.class; } public void invoke (FilterInvocation fi) throws IOException, ServletException { InterceptorStatusToken token = super .beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super .afterInvocation(token, null ); } } @Override public SecurityMetadataSource obtainSecurityMetadataSource () { return this .securityMetadataSource; } public void destroy () { } public void init (FilterConfig filterconfig) throws ServletException { } }
然后是把我们自己的权限数据加载到Spring Security中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 @Component public class DemoInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource { private static Map<String, Collection<ConfigAttribute>> resourceMap = null ; public DemoInvocationSecurityMetadataSourceService () { } private void loadResourceDefine () { Role r = new Role(); r.setId(0L ); r.setName("admin" ); List<Role> roles = Collections.singletonList(r); resourceMap = new HashMap<>(); for (Role role : roles) { ConfigAttribute ca = new SecurityConfig(role.getName()); Map<String, Object> params = new HashMap<>(); params.put("roleId" , role.getId()); List<String> resources = Collections.singletonList("/user/*" ); for (String url : resources) { if (resourceMap.containsKey(url)) { Collection<ConfigAttribute> value = resourceMap.get(url); value.add(ca); resourceMap.put(url, value); } else { Collection<ConfigAttribute> atts = new ArrayList<>(); atts.add(ca); resourceMap.put(url, atts); } } } } @Override public Collection<ConfigAttribute> getAllConfigAttributes () { loadResourceDefine(); return null ; } @Override public Collection<ConfigAttribute> getAttributes (Object object) throws IllegalArgumentException { FilterInvocation filterInvocation = (FilterInvocation) object; for (String url : resourceMap.keySet()) { RequestMatcher requestMatcher = new AntPathRequestMatcher(url); HttpServletRequest httpRequest = filterInvocation.getHttpRequest(); if (requestMatcher.matches(httpRequest)) { return resourceMap.get(url); } } return null ; } @Override public boolean supports (Class<?> arg0) { return true ; } }
现在我们拿到了用户的角色,也拿到了系统里有的角色和权限,就需要判断他是否有这个权限了,配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 @Component public class DemoAccessDecisionManager implements AccessDecisionManager { public void decide (Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if (configAttributes == null ) { return ; } for (ConfigAttribute ca : configAttributes) { String needRole = ca.getAttribute(); for (GrantedAuthority ga : authentication.getAuthorities()) { if (needRole.trim().equals(ga.getAuthority().trim())) { return ; } } } throw new AccessDeniedException("没有权限进行操作!" ); } public boolean supports (ConfigAttribute attribute) { return true ; } public boolean supports (Class<?> clazz) { return true ; } }
我们试试登录的接口:
然后我们用这个token来调用另外一个接口。 我们先试试不传Token会返回什么
判断没有登录,现在再来试试带上token的请求。 已经成功的请求到了数据。
好了,核心配置就是这些,我把这些代码上传github上,有需要的可以下载下来看看。里面的角色和权限都是虚拟数据,应用还需要自行修改代码。https://github.com/sail-y/spring-boot-jwt