在上一篇基础上继续集成 JWT ,实现用户身份验证。
前后端分离项目中,如果直接把 API 接口对外开放,我们知道这样风险是很大的,所以在上一篇中我们引入了 Spring Security ,但是我们在登陆后缺少了请求凭证部分。
JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。
1、用户进入登录页,输入用户名、密码,进行登录; 2、服务器验证登录鉴权,如果改用户合法,根据用户的信息和服务器的规则生成 JWT Token 3、服务器将该 token 以 json 形式返回(不一定要json形式,这里说的是一种常见的做法) 4、用户得到 token,存在 localStorage、cookie 或其它数据存储形式中。以后用户请求 /protected 中的 API 时,在请求的 header 中加入 Authorization: Bearer xxxx(token)。此处注意token之前有一个7字符长度的 Bearer。 5、服务器端对此 token 进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。 6、用户取得结果
如下如所示:
7790cc3aade467c985e2e4a8105b89f1.png
来看一下 JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
token 分成了三部分,头部(header),荷载(Payload) 和 签名(Signature),每部分用 . 分隔,其中头部和荷载使用了base64编码,分别解码之后得到两个JSON串:
第一部分-头部:
{ "alg": "HS256", "typ": "JWT" }
alg字段为加密算法,这是告诉我们 HMAC 采用 HS512 算法对 JWT 进行的签名。
第二部分-荷载:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
荷载的字段及含义:
这段告诉我们这个Token中含有的数据声明(Claim),这个例子里面有三个声明:sub, name 和 iat。在我们这个例子中,分别代表着 所面向的用户、用户名、创建时间,当然你可以把任意数据声明在这里。
第三部分-签名:
第三部分签名则不能使用base64解码出来,该部分用于验证头部和荷载数据的完整性。
引入依赖:
<!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
创建一个测试类尝试一下 JWT 的生成:
public class Test { public static void main(String[] args){ String token = Jwts.builder() 主题 放入用户名 .setSubject("niceyoo") 自定义属性 放入用户拥有请求权限 .claim("authorities","admin") 失效时间 .setExpiration(new Date(System.currentTimeMillis() + 7 * 60 * 1000)) 签名算法和密钥 .signWith(SignatureAlgorithm.HS512, "tmax") .compact(); System.out.println(token); } }
控制台打印如下:
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJuaWNleW9vIiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU1OTQ1ODM1M30.keCiHrcEr0IWXfZLocgHS8znn7uSiaZW1IT6bTs-EQG0NPsb6-Aw_XbGQea4mez2CcAflgMqtzIpsDjZsUOVug
数据声明(Claim)是一个自定义属性,可以用来放入用户拥有请求权限。上边为简单直接传了一个 'admin'。
再看看解析:
public static void main(String[] args){ try { 解析token Claims claims = Jwts.parser() .setSigningKey("tmax") .parseClaimsJws("eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJuaWNleW9vIiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU1OTQ1OTc2Mn0.MkSJtGaVePLa-eM3gylh1T3fwODg-6ceDDOxscXAQKun-qNrbQFcKPNqXhblbXPNLhaJyEnwugNANCTs98UNmA") .getBody(); System.out.println(claims); 获取用户名 String username = claims.getSubject(); System.out.println("username:"+username); 获取权限 String authority = claims.get("authorities").toString(); System.out.println("权限:"+authority); } catch (ExpiredJwtException e) { System.out.println("jwt异常"); } catch (Exception e){ System.out.println("异常"); } }
控制台打印:
{sub=niceyoo, authorities=admin, exp=1559459762} username:niceyoo 权限:admin
JWT 本身没啥难度,但安全整体是一个比较复杂的事情,JWT 只不过提供了一种基于 token 的请求验证机制。但我们的用户权限,对于 API 的权限划分、资源的权限划分,用户的验证等等都不是JWT负责的。也就是说,请求验证后,你是否有权限看对应的内容是由你的用户角色决定的。所接下来才是我们的重点,Spring Security 整合 JWT。
要想要 JW T在 Spring 中工作,我们应该新建一个 JWT filter,并把它配置在 WebSecurityConfig 中。
WebSecurityConfigurerAdapter.java
@Slf4j @Configuration @EnableGlobalMethodSecurity(prePostEnabled=true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; @Autowired private AuthenticationSuccessHandler successHandler; @Autowired private AuthenticationFailHandler failHandler; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());加密 } @Override protected void configure(HttpSecurity http) throws Exception { ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http .authorizeRequests(); registry.and() 表单登录方式 .formLogin() .permitAll() 成功处理类 .successHandler(successHandler) 失败 .failureHandler(failHandler) .and() .logout() .permitAll() .and() .authorizeRequests() 任何请求 .anyRequest() 需要身份认证 .authenticated() .and() 关闭跨站请求防护 .csrf().disable() 前后端分离采用JWT 不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() 添加JWT过滤器 除已配置的其它请求都需经过此过滤器 .addFilter(new JWTAuthenticationFilter(authenticationManager(), 7)); } }
相较于上一篇主要多了如下一行配置:
.addFilter(new JWTAuthenticationFilter(authenticationManager(), 7));
JWTAuthenticationFilter.java
@Slf4j public class JWTAuthenticationFilter extends BasicAuthenticationFilter { private Integer tokenExpireTime; public JWTAuthenticationFilter(AuthenticationManager authenticationManager, Integer tokenExpireTime) { super(authenticationManager); this.tokenExpireTime = tokenExpireTime; } public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) { super(authenticationManager, authenticationEntryPoint); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header = request.getHeader(SecurityConstant.HEADER); if(StrUtil.isBlank(header)){ header = request.getParameter(SecurityConstant.HEADER); } Boolean notValid = StrUtil.isBlank(header) || (!header.startsWith(SecurityConstant.TOKEN_SPLIT)); if (notValid) { chain.doFilter(request, response); return; } try { UsernamePasswordAuthenticationToken 继承 AbstractAuthenticationToken 实现 Authentication 所以当在页面中输入用户名和密码之后首先会进入到 UsernamePasswordAuthenticationToken验证(Authentication), UsernamePasswordAuthenticationToken authentication = getAuthentication(header, response); SecurityContextHolder.getContext().setAuthentication(authentication); }catch (Exception e){ e.toString(); } chain.doFilter(request, response); } private UsernamePasswordAuthenticationToken getAuthentication(String header, HttpServletResponse response) { 用户名 String username = null; 权限 List<GrantedAuthority> authorities = new ArrayList<>(); try { 解析token Claims claims = Jwts.parser() .setSigningKey(SecurityConstant.JWT_SIGN_KEY) .parseClaimsJws(header.replace(SecurityConstant.TOKEN_SPLIT, "")) .getBody(); logger.info("claims:"+claims); 获取用户名 username = claims.getSubject(); logger.info("username:"+username); 获取权限 String authority = claims.get(SecurityConstant.AUTHORITIES).toString(); logger.info("authority:"+authority); if(!StringUtils.isEmpty(authority)){ authorities.add(new SimpleGrantedAuthority(authority)); } } catch (ExpiredJwtException e) { ResponseUtil.out(response, ResponseUtil.resultMap(false,401,"登录已失效,请重新登录")); } catch (Exception e){ log.error(e.toString()); ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"解析token错误")); } if(StrUtil.isNotBlank(username)) { 踩坑提醒 此处password不能为null User principal = new User(username, "", authorities); return new UsernamePasswordAuthenticationToken(principal, null, authorities); } return null; } }
接下来我们启动项目看看:
访问项目中已有的链接:
http://localhost:7777/tmax/videoCategory/getAll
老样子认证一波:
其中 niceyoo、 为数据库用户信息
登陆成功后获取返回的 token,注意,此 token 是由 JWT 生成的:
String token = SecurityConstant.TOKEN_SPLIT + Jwts.builder() 主题 放入用户名 .setSubject(username) 自定义属性 放入用户拥有请求权限 .claim(SecurityConstant.AUTHORITIES, authorities) 失效时间 .setExpiration(new Date(System.currentTimeMillis() + 7 * 60 * 1000)) 签名算法和密钥 .signWith(SignatureAlgorithm.HS512, SecurityConstant.JWT_SIGN_KEY) .compact();
浏览器返回 token 如下:
ad45accf0b31c606a10c568acbddec19.png
然后我们通过 token 凭证去访问上边的方法:
e3c05b207e1ec11e1f23560ef1b724b6.png
后台打印信息:
claims:{sub=niceyoo, authorities=admin, exp=1559472866} username:niceyoo authority:admin
随便改一下 token ,返回如下:
18年专科毕业后,期间一度迷茫,最近我创建了一个公众号用来记录自己的成长。
原文链接:https://www.cnblogs.com/niceyoo/p/10964277.html