当前位置:   article > 正文

后端架构token授权认证机制:spring security JSON Web Token(JWT)简例_usernamepasswordauthenticationtoken

usernamepasswordauthenticationtoken

后端架构token授权认证机制:spring security JSON Web Token(JWT)简例

在基于token的客户端-服务器端认证授权以前,前端到服务器端的认证-授权通常是基于session,自从token机制出现并流行起来后,基于token的客户端-服务器端认证-授权访问机制变得越来越主要,token机制从某种意义上讲是过去传统session会话机制的另外一种解决方案,并尤其适用于当前的大规模微服务、分布式体系架构。

客户端(浏览器web前端、app移动端等)与后端服务基于token的认证-授权访问流程一般情况是这样的:

(1)用户侧发送用户名和密码到服务器端。
(2)服务器端收到用户名后验证用户有效性。
(3)服务器端验证通过后,发送给用户一个token。
(4)客户端存储token,并在每次请求服务器端时附带上这个token。
(5)后续,每一层客户端的请求到达服务器端后,服务器验证token的有效性(合法性),若通过验证,返回客户端所需数据。

JWT可以认为是一种特殊编码格式的token。普通oauth2颁发是一串随机hash字符串,本身无意义,而JWT的token有特定含义,分为三部分:

(1)头部Header
(2)载荷Payload
(3)签名Signature

每一部分用 . 分隔开。

典型的应用场景是api鉴权。比如移动应用的app开发,用api,从远程服务器端拉取数据,每次的http访问,均带上token。

JWT-token此类鉴权认证机制不太适用的场景:

(1)传统session的会话保持业务逻辑。因为token无状态、分布式,在token有效期内,原则上只要使用这个token,仍然可以访问系统。
(2)token续签。围绕token续签设计系统架构会增加额外的复杂度。可以用redis记录token状态,在用户访问后更新状态,但这就是token机制用“歪”了,JWT-token的无状态此时变成有状态了,而这恰恰就是传统session+cookie机制可以覆盖住的业务场景。考虑系统的拓展和高可用,可以考虑使用成熟的spring session框架(B/S应用场景)。

现在写一个例子,代码如下:

 其中最关键的有三个类,JwtAuthenticationController,JwtRequestFilter,JwtWebSecurityConfigurerAdapter

这三个类涉及到spring boot对于token生成和验证的逻辑流程。结合上面的逻辑代码,简单总结一下spring+jwt这种组合框架是如何验证-生成授权token。

如果不使用spring security,那么spring里面实现接口访问即是常规的框架编程。引入spring security后,并在spring security中实现基于JWT的token授权-验证,那么首先需要对于访问用户发放token。所以在JwtAuthenticationController的createAuthenticationToken实现对于用户token的生成和返回。

当用户访问localhost:8080/auth后(注意是POST方法,需要写入用户名和密码),JwtAuthenticationController在createAuthenticationToken里面提取用户名和密码,构造UsernamePasswordAuthenticationToken,并将UsernamePasswordAuthenticationToken记入到上下文中的authenticationManager,如果用户名和密码均正确,spring security就“记忆”当前访问的用户,并通过jwtInMemoryUserDetailsService加载用户信息,然后jwtTokenUtil生成token返回给用户。

此后,客户端用户每一次访问受保护的资源时候,加入token。token是写到header里面的,token的key为Authorization,value按照协议规范,需要以字符串Bearer 开头,注意Bearer后面要带上空格。

JwtRequestFilter类目的是配置spring的http请求,把JwtRequestFilter写完后,加入到JwtWebSecurityConfigurerAdapter类的configure(HttpSecurity http)函数的http里面:

http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

加入后,spring接受的http请求,会被JwtRequestFilter过滤一次,JwtRequestFilter的过滤核心是doFilterInternal函数。doFilterInternal对于每一次的http请求,均会提取http头部header字段里面的key为Authorization对应的值,如果对应的值非空,且以Bearer 开头,则意味是token认证,那么就走token认证逻辑。代码将会根据传入的token逆向的通过工具类jwtTokenUtil反向找出用户名,然后根据用户名判断当前用户是否已被授权,如果没有被授权,jwtTokenUtil验证客户端传入的token和后端系统的token信息是否一致,一致则把具有该token的客户端记录进spring security授权记录中,从此,凡是具有该token的访问,均放行通过。

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.http.ResponseEntity;
  3. import org.springframework.security.authentication.AuthenticationManager;
  4. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  5. import org.springframework.security.core.userdetails.UserDetails;
  6. import org.springframework.web.bind.annotation.RequestBody;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import org.springframework.web.bind.annotation.RequestMethod;
  9. import org.springframework.web.bind.annotation.RestController;
  10. import java.util.HashMap;
  11. import java.util.Map;
  12. @RestController
  13. public class JwtAuthenticationController {
  14. @Autowired
  15. private AuthenticationManager authenticationManager;
  16. @Autowired
  17. private JwtTokenUtil jwtTokenUtil;
  18. @Autowired
  19. private JwtUserDetailsService jwtInMemoryUserDetailsService;
  20. /**
  21. * 验证用户名和密码。
  22. * 如果验证通过,创建token并将其返回给客户端
  23. *
  24. * @param request
  25. * @return
  26. */
  27. @RequestMapping(value = "/auth", method = RequestMethod.POST)
  28. public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest request) {
  29. String username = request.getUsername();
  30. String password = request.getPassword();
  31. System.out.println("用户名:" + username);
  32. System.out.println("密码:" + password);
  33. UsernamePasswordAuthenticationToken authentication=new UsernamePasswordAuthenticationToken(username, password);
  34. authenticationManager.authenticate(authentication);
  35. UserDetails userDetails = jwtInMemoryUserDetailsService.loadUserByUsername(request.getUsername());
  36. String token = jwtTokenUtil.generateToken(userDetails);
  37. return ResponseEntity.ok(new JwtResponse(token));
  38. }
  39. @RequestMapping(value="/api")
  40. public Map<String, String> api() {
  41. Map<String,String> map=new HashMap<String,String>();
  42. map.put("page", "api");
  43. return map;
  44. }
  45. @RequestMapping(value="/index")
  46. public Map<String, String> index() {
  47. Map<String,String> map=new HashMap<String,String>();
  48. map.put("page", "index");
  49. return map;
  50. }
  51. @RequestMapping(value="/home")
  52. public Map<String, String> home() {
  53. Map<String,String> map=new HashMap<String,String>();
  54. map.put("page", "home");
  55. return map;
  56. }
  57. }

  1. import org.springframework.security.core.AuthenticationException;
  2. import org.springframework.security.web.AuthenticationEntryPoint;
  3. import org.springframework.stereotype.Component;
  4. import javax.servlet.ServletException;
  5. import javax.servlet.http.HttpServletRequest;
  6. import javax.servlet.http.HttpServletResponse;
  7. import java.io.IOException;
  8. /**
  9. * 拒绝每个没有通过身份验证的请求并发送错误代码401。
  10. */
  11. @Component
  12. public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
  13. @Override
  14. public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
  15. System.out.println("未获得授权");
  16. response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
  17. }
  18. }

  1. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  2. import org.springframework.stereotype.Component;
  3. /**
  4. * 注意,此处是明文,实际场景时候要加密
  5. */
  6. @Component
  7. public class JwtPasswordEncoder extends BCryptPasswordEncoder {
  8. @Override
  9. public String encode(CharSequence rawPassword) {
  10. return rawPassword.toString();
  11. }
  12. @Override
  13. public boolean matches(CharSequence rawPassword, String encodedPassword) {
  14. if (rawPassword == null) {
  15. throw new IllegalArgumentException("rawPassword为空!");
  16. }
  17. if (encodedPassword == null || encodedPassword.length() == 0) {
  18. throw new IllegalArgumentException("encodedPassword为空");
  19. }
  20. return encodedPassword.equals(rawPassword);
  21. }
  22. }

  1. import java.io.Serializable;
  2. public class JwtRequest implements Serializable {
  3. private String username;
  4. private String password;
  5. //JSON Parsing
  6. public JwtRequest() {
  7. }
  8. public JwtRequest(String username, String password) {
  9. this.setUsername(username);
  10. this.setPassword(password);
  11. }
  12. public String getUsername() {
  13. return this.username;
  14. }
  15. public void setUsername(String username) {
  16. this.username = username;
  17. }
  18. public String getPassword() {
  19. return this.password;
  20. }
  21. public void setPassword(String password) {
  22. this.password = password;
  23. }
  24. }

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  3. import org.springframework.security.core.context.SecurityContextHolder;
  4. import org.springframework.security.core.userdetails.UserDetails;
  5. import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
  6. import org.springframework.stereotype.Component;
  7. import org.springframework.web.filter.OncePerRequestFilter;
  8. import javax.servlet.FilterChain;
  9. import javax.servlet.ServletException;
  10. import javax.servlet.http.HttpServletRequest;
  11. import javax.servlet.http.HttpServletResponse;
  12. import java.io.IOException;
  13. /**
  14. * 对于任何一个传入的请求,都会执行doFilterInternal。
  15. * 检查请求是否具有有效的token。
  16. * 如果token有效,在上下文中设置Authentication,
  17. * 表明当前用户通过身份验证。
  18. */
  19. @Component
  20. public class JwtRequestFilter extends OncePerRequestFilter {
  21. @Autowired
  22. private JwtUserDetailsService jwtUserDetailsService;
  23. @Autowired
  24. private JwtTokenUtil jwtTokenUtil;
  25. @Override
  26. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  27. String header = request.getHeader("Authorization");
  28. String username = null;
  29. String token = null;
  30. // token一般又称之为"Bearer token",以Bearer开头
  31. // 截取纯粹的token
  32. String BEARER = "Bearer ";
  33. if (header != null && header.startsWith(BEARER)) {
  34. token = header.substring(BEARER.length());//从Bearer 之后开始截取
  35. username = jwtTokenUtil.getUsernameFromToken(token);
  36. System.out.println(username + " token:" + token);
  37. }
  38. //拿到token后验证
  39. if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
  40. UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(username);
  41. // 如果token有效,配置Spring Security授权
  42. if (jwtTokenUtil.validateToken(token, userDetails)) {
  43. System.out.println(username + " token验证通过");
  44. UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
  45. authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
  46. //当前用户已经授权,授权认证信息传递给Spring Security配置.
  47. SecurityContextHolder.getContext().setAuthentication(authToken);
  48. }
  49. }
  50. if (SecurityContextHolder.getContext().getAuthentication() != null) {
  51. System.out.println(username + " 已授权");
  52. }
  53. filterChain.doFilter(request, response);
  54. }
  55. }

  1. public class JwtResponse {
  2. private String token;
  3. public JwtResponse(String token) {
  4. this.token = token;
  5. }
  6. public String getToken() {
  7. return token;
  8. }
  9. }

  1. import io.jsonwebtoken.Claims;
  2. import io.jsonwebtoken.Jwts;
  3. import io.jsonwebtoken.SignatureAlgorithm;
  4. import org.springframework.security.core.userdetails.UserDetails;
  5. import org.springframework.stereotype.Component;
  6. import java.io.Serializable;
  7. import java.util.Date;
  8. import java.util.HashMap;
  9. import java.util.Map;
  10. import java.util.function.Function;
  11. @Component
  12. public class JwtTokenUtil implements Serializable {
  13. public static final long TOKEN_VALIDITY = 60 * 60 * 5; //token有效期
  14. private String secret = "zhangphil_secret";
  15. public String getUsernameFromToken(String token) {
  16. return getClaimFromToken(token, Claims::getSubject);
  17. }
  18. public Date getIssuedAtDateFromToken(String token) {
  19. return getClaimFromToken(token, Claims::getIssuedAt);
  20. }
  21. public Date getExpirationDateFromToken(String token) {
  22. return getClaimFromToken(token, Claims::getExpiration);
  23. }
  24. public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
  25. final Claims claims = getAllClaimsFromToken(token);
  26. return claimsResolver.apply(claims);
  27. }
  28. private Claims getAllClaimsFromToken(String token) {
  29. return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
  30. }
  31. private Boolean isTokenExpired(String token) {
  32. final Date expiration = getExpirationDateFromToken(token);
  33. return expiration.before(new Date());
  34. }
  35. private Boolean ignoreTokenExpiration(String token) {
  36. return false;
  37. }
  38. public String generateToken(UserDetails userDetails) {
  39. Map<String, Object> claims = new HashMap<>();
  40. return doGenerateToken(claims, userDetails.getUsername());
  41. }
  42. private String doGenerateToken(Map<String, Object> claims, String subject) {
  43. return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
  44. .setExpiration(new Date(System.currentTimeMillis() + TOKEN_VALIDITY * 1000)).signWith(SignatureAlgorithm.HS512, secret).compact();
  45. }
  46. public Boolean canTokenBeRefreshed(String token) {
  47. return (!isTokenExpired(token) || ignoreTokenExpiration(token));
  48. }
  49. public Boolean validateToken(String token, UserDetails userDetails) {
  50. final String username = getUsernameFromToken(token);
  51. return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
  52. }
  53. }

  1. import org.springframework.security.core.userdetails.User;
  2. import org.springframework.security.core.userdetails.UserDetails;
  3. import org.springframework.security.core.userdetails.UserDetailsService;
  4. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  5. import org.springframework.stereotype.Service;
  6. import java.util.ArrayList;
  7. @Service
  8. public class JwtUserDetailsService implements UserDetailsService {
  9. //正常情况下,当loadUserByUsername传入用户名后,
  10. //应该连接数据库从数据库中根据用户名把该用户的信息取出来,
  11. //本例出于简单演示的目的,不再额外的引入数据库,
  12. //假设已经知道用户名和密码,硬编码写死了用户名和密码
  13. public static final String USER_NAME = "zhangphil";
  14. public static final String USER_PASSWORD = "12345678";
  15. @Override
  16. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  17. System.out.println(username + " 加载信息");
  18. if (USER_NAME.equals(username)) {
  19. return new User(USER_NAME, USER_PASSWORD, new ArrayList<>());
  20. } else {
  21. throw new UsernameNotFoundException("用户不存在:" + username);
  22. }
  23. }
  24. }

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.authentication.AuthenticationManager;
  5. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  6. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
  7. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  8. import org.springframework.security.config.annotation.web.builders.WebSecurity;
  9. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  10. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  11. import org.springframework.security.config.http.SessionCreationPolicy;
  12. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  13. @Configuration
  14. @EnableWebSecurity
  15. @EnableGlobalMethodSecurity(prePostEnabled = true)
  16. public class JwtWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
  17. /**
  18. * 密码管理
  19. */
  20. @Autowired
  21. private JwtPasswordEncoder jwtPasswordEncoder;
  22. @Autowired
  23. private JwtUserDetailsService jwtUserDetailsService;
  24. @Autowired
  25. private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
  26. @Autowired
  27. private JwtRequestFilter jwtRequestFilter;
  28. @Override
  29. protected void configure(HttpSecurity http) throws Exception {
  30. // 本例不需要CSRF
  31. http.csrf().disable()
  32. // 排除/auth。
  33. // 对于请求授权的/auth不需要授权,放行。
  34. .authorizeRequests()
  35. .antMatchers("/auth").permitAll()
  36. //其余的所有请求均需要认证授权
  37. .anyRequest().authenticated()
  38. .and()
  39. .exceptionHandling()//错误处理
  40. .authenticationEntryPoint(jwtAuthenticationEntryPoint)
  41. .and()
  42. //本例不需要维护有状态的session
  43. .sessionManagement()
  44. .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  45. // 对于任何一个传入的请求加入一个token过滤器,验证。
  46. http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
  47. }
  48. /**
  49. * 配置AuthenticationManager使其知道从那里加载用户认证信息
  50. */
  51. @Override
  52. public void configure(AuthenticationManagerBuilder auth) {
  53. try {
  54. auth.userDetailsService(jwtUserDetailsService)
  55. .passwordEncoder(jwtPasswordEncoder);
  56. } catch (Exception e) {
  57. e.printStackTrace();
  58. }
  59. }
  60. /**
  61. * 假设这一部分接口是公开开放的,不需要token即可访问。
  62. * 这部分客户端http请求不拦截
  63. * 排除。
  64. */
  65. @Override
  66. public void configure(WebSecurity web) {
  67. web.ignoring()
  68. .antMatchers(
  69. "/index**",
  70. "/home/**");
  71. }
  72. @Bean
  73. @Override
  74. public AuthenticationManager authenticationManagerBean() throws Exception {
  75. return super.authenticationManagerBean();
  76. }
  77. }

  1. import org.springframework.boot.SpringApplication;
  2. import org.springframework.boot.autoconfigure.SpringBootApplication;
  3. @SpringBootApplication
  4. public class SpringJwtApplication {
  5. public static void main(String[] args) {
  6. SpringApplication.run(SpringJwtApplication.class, args);
  7. }
  8. }

系统启动后,当访问/index或者/home页面时候,不需要token,均正常打开:

当直接访问localhost:8080/api时候,页面返回HTTP ERROR 401错误,此时,需要post用户名和密码:

然后后台返回token:

把token复制出来,在get请求时候,加入token,访问/api接口,注意,需要为token加上头Bearer ,加到header里面,key是Authorization:

后台系统返回正确结果:

与此同时,系统的后台日志输出为:

  1. 未获得授权
  2. 用户名:zhangphil
  3. 密码:12345678
  4. zhangphil 加载信息
  5. zhangphil 加载信息
  6. zhangphil token:eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ6aGFuZ3BoaWwiLCJleHAiOjE2NDQ1MDc3OTcsImlhdCI6MTY0NDQ4OTc5N30.90WpWZ4MBk1bsEd8r7Vmz7MHQ350q_DEISC7mvHmamgU1YNM_ppfyYweUsb0MxRD-qPFdHF7fNBbf-4H0u6wyw
  7. zhangphil 加载信息
  8. zhangphil token验证通过
  9. zhangphil 已授权

如果在授权认证时候,传入错误的密码,比如密码错误的是1234,那么系统认证失败,后台日志输出:

  1. 用户名:zhangphil
  2. 密码:1234
  3. zhangphil 加载信息
  4. 未获得授权

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/301622
推荐阅读
相关标签
  

闽ICP备14008679号