为了提高网站的api的安全性,一般都会实现认证(Autentication)和授权(Authenrization),分别对应用户的登录、控制用户可以访问的资源。
JWT介绍 什么是JWT(Json Web Token) 详情请见jwt官网 .直观来讲,它是一个格式为形如aaaa.bbbb.cccc
的字符串token,它存放了登录用户的信息,也是用户登录网站的凭证。
为什么要用JWT? 当我们登录了一个网站,然后因为手头有急事把浏览器关闭转而做其他事,但你不用担心下次重新打开浏览器后网站已经失去了登录状态(前提是你间隔的时间没有大于jwt的过期时间),这很方便。当然你会说,Session也可以实现这个功能,But在使用Session的同时,会增加服务器的存储压力,而JWT是将存储的压力分布到各个客户端,减轻服务器的压力。
JWT长什么样 JWT由三个子字符串组成
Header(头部)
Payload(负载)
Signature(签名)
写成一行,就是这个样子:Header.Payload.Signature
Header
Header是由一下这个格式的Json通过Base64编码生成的字符串(注意:Base64编码不是加密,加密一般不可破解,编码可以通过用编码反编码或得原来的Json数据,因此不安全,所以不存放敏感信息 ),Header中存放的内容是说明编码对象是一个JWT以及使用“SHA-256”的算法进行加密(加密用于生成Signature签名)
1 2 3 4 { "alg" : "HS256" , "typ" : "JWT" }
Payload
虽然第二部分官网上写的是Payload,但你要认识到Payload是base64编码得到的字符串,编码之前它是个Json格式的数据,我们把它称作Claim,也就是Claim---Base64编码-->Payload
.这也很好理解,那Claim里存放的是什么数据呢,它存放JWT自身的标准属性,所有的标准属性都是可选的,比如有:
“iss”:”Issuer —— 签发人”,
“sub”:”Subject —— 主题”,
“aud”:”Audience —— 受众”,
“exp”:”Expiration Time —— 过期时间”,
“nbf”:”Not Before —— 生效时间”,
“iat”:”Issued At —— 签发时间”,
“jti”:”JWT ID —— 编号”
除了以上的JWT官方标准属性,你还可以在这个部分添加自定义字段,以下的例子中admin
即自定义字段。
1 2 3 4 5 { "sub" : "1234567890" , "name" : "John Doe" , "admin" : true }
Signature
Signature 是由Header和Payload组合而成,将Header和Claim这两个Json分别使用Base64方式进行编码,生成字符串Header和Payload,然后将Header和Payload以Header.Payload的格式组合在一起形成一个字符串,然后使用Header里指定的签名算法和一个密匙(这个secret存放在服务器上,用于进行验证)对这个字符串进行加密,形成一个新的字符串,公式如下
1 2 3 4 HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
SpringBoot使用JWT SpringBoot整合JWT的方式有很多,也有第三方的库,比如auth0,但是我们不使用。我们最终需要实现两个功能,一个是实现用户登录(Authentication),另一个是用户授权(Authenrization)。工程目录结构如下:
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 . └── src ├── main │ ├── java │ │ └── com │ │ └── bababadboy │ │ ├── dealermng │ │ │ ├── Application.java │ │ │ ├── config │ │ │ │ ├── SecurityConfig.java │ │ │ │ └── WebMvcConfigurer.java │ │ │ ├── controller │ │ │ │ ├── ProductController.java │ │ │ │ └── UserController.java │ │ │ ├── dto │ │ │ │ └── UserDataDTO.java │ │ │ ├── entity │ │ │ │ ├── Product.java │ │ │ │ └── user │ │ │ │ ├── Role.java │ │ │ │ └── User.java │ │ │ ├── repository │ │ │ │ ├── ProductRepository.java │ │ │ │ └── UserRepository.java │ │ │ ├── security │ │ │ │ ├── JwtCfg.java │ │ │ │ ├── JwtFilter.java │ │ │ │ ├── JwtTokenFilterConfigurer.java │ │ │ │ ├── JwtTokenProvider.java │ │ │ └── service │ │ │ ├── ProductService.java │ │ │ └── impl │ │ │ ├── ProductServiceImpl.java │ │ │ └── UserService.java │ │ └── springboot1 │ └── resources ——...
JwtCfg类 这个类中声明了一个@Bean ,用于生成一个过滤器类,对/api 链接下的所有资源访问进行JWT的验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration public class JwtCfg { @Bean public FilterRegistrationBean<JwtFilter> jwtFilter () { FilterRegistrationBean<JwtFilter> registrationBean; registrationBean = new FilterRegistrationBean<JwtFilter>(); registrationBean.setFilter(new JwtFilter()); registrationBean.addUrlPatterns("/api/*" ); return registrationBean; } }
JwtFilter类 这个类声明了一个JWT过滤器类,从Http请求中提取JWT的信息,并使用了”secretkey”这个密匙对JWT进行验证
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 public class JwtFilter extends GenericFilterBean { public void doFilter (final ServletRequest req, final ServletResponse res, final FilterChain chain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) req; final HttpServletResponse response = (HttpServletResponse) res; final String authHeader = request.getHeader("authorization" ); if ("OPTIONS" .equals(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); chain.doFilter(req, res); } else { if (authHeader == null || !authHeader.startsWith("Bearer " )) { throw new ServletException("Missing or invalid Authorization header" ); } final String token = authHeader.substring(7 ); try { final Claims claims = Jwts.parser().setSigningKey("secretkey" ).parseClaimsJws(token).getBody(); request.setAttribute("claims" , claims); } catch (final SignatureException e) { throw new ServletException("Invalid token" ); } chain.doFilter(req, res); } } }
UserController类,用户的注册和登录。
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 @RestController @RequestMapping ("/users" )public class UserController { private final UserService userService; private final ModelMapper modelMapper; @Autowired public UserController (UserService userService, ModelMapper modelMapper) { this .userService = userService; this .modelMapper = modelMapper; } @PostMapping ("/signup" ) public String signUp (@RequestBody User user) { return userService.signUp(user); } @PostMapping ("/login" ) public Object logIn (@RequestParam String username,@RequestParam String password) { String token = userService.logIn(username,password); UserDataDTO info = modelMapper.map(userService.search(username),UserDataDTO.class); JSONObject result = new JSONObject(); result.put("userMsg" ,info); result.put("accessToken" ,token); return JSON.toJSON(result); } }
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 @Component public class JwtTokenProvider { @Value ("${security.jrwt.token.secret-key:secret-key}" ) private String secretKey; @Value ("${security.jwt.token.expire-length:3600000}" ) private long validityInMilliseconds = 3600000 ; @Autowired private MyUserDetails myUserDetails; @PostConstruct protected void init () { secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); } public String createToken (String username, List<Role> roles) { Claims claims = Jwts.claims().setSubject(username); claims.put("auth" , roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList())); Date now = new Date(); Date validity = new Date(now.getTime() + validityInMilliseconds); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(validity) .signWith(SignatureAlgorithm.HS256, "secretkey" ) .compact(); } public Authentication getAuthentication (String token) { UserDetails userDetails = myUserDetails.loadUserByUsername(getUsername(token)); return new UsernamePasswordAuthenticationToken(userDetails, "" , userDetails.getAuthorities()); } public String getUsername (String token) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); } public String resolveToken (HttpServletRequest req) { String bearerToken = req.getHeader("Authorization" ); if (bearerToken != null && bearerToken.startsWith("Bearer " )) { return bearerToken.substring(7 , bearerToken.length()); } return null ; } public boolean validateToken (String token) { try { Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); return true ; } catch (JwtException | IllegalArgumentException e) { throw new CustomException("Expired or invalid JWT token" , HttpStatus.INTERNAL_SERVER_ERROR); } } }
ProductController类,测试JWT功能,只有当用户认证成功之后,/api
下的资源才能被访问。
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 @Transactional @RestController @RequestMapping (value = "/api" )public class ProductController { private final ProductRepository productRepository; private final ProductServiceImpl productService; @Autowired public ProductController (ProductRepository productRepository, ProductServiceImpl productService) { this .productRepository = productRepository; this .productService = productService; } @RequestMapping (value = "/products" ,method = RequestMethod.GET) public Object retrieveAllProducts (@RequestParam(value = "page" , defaultValue = "0" ) Integer page , @RequestParam (value = "size" , defaultValue = "15" ) Integer size) { return JSON.toJSON(productService.findProductNoCriteria(page,size).getContent()); } @GetMapping (value = "/products/{no}" ) public Object retrieveProduct (@PathVariable("no" ) String no) { Optional<Product> product = productService.retrieveProduct(no); return JSON.toJSON(product); } }
代码功能测试
首先直接访问/api
下的product资源,毫无疑问是不能访问的。
然后进行一个新的测试用户的注册,可以看到注册成功的提示返回
再让该用户进行登录,可以看到登录成功之后返回的JWT字符串
最后用获取到的JWT作为访问令牌,申请访问/api/products,可以访问成功。
参考链接:https://jwt.io/ http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html https://blog.csdn.net/ltl112358/article/details/79507148