• Springboot Security 前后端分离模式自由接口最小工作模型


    但凡讲解Springboot Security的教程,都是根据其本身的定义,前后端整合在一起,登录采用form或者basic。我们现在的很多项目,前后端分离,form登录已经不适用了。很多程序的架构要求所有的接口都采用application/json方式,因此basic登录模式也几乎不用。我们需要纯粹使用自己的自由接口来实现注册登录,以及其他业务接口访问的身份验证和授权。这里的设计是用户身份验证与授权的模块跟业务模块的身份权限验证是分开的。不过为了紧凑,我把两部分放在一起做了一个最小工作模型。

     这是最小模型的基本结构,核心的类就config中两个。其他的都是工作模型。

    一、创建Springboot web项目,添加pom

    1. <dependency>
    2. <groupId>org.springframework.bootgroupId>
    3. <artifactId>spring-boot-starter-webartifactId>
    4. dependency>
    5. <dependency>
    6. <groupId>org.springframework.bootgroupId>
    7. <artifactId>spring-boot-starter-securityartifactId>
    8. dependency>
    9. <dependency>
    10. <groupId>io.jsonwebtokengroupId>
    11. <artifactId>jjwtartifactId>
    12. <version>0.9.1version>
    13. dependency>

    二、一个用户构建token签发数据的类

    1. package com.chris.demo.domain;
    2. /**
    3. * @author Chris Chan
    4. * Create On 2022/11/24 10:19
    5. * Use for:
    6. * Explain:
    7. */
    8. public class JwtResult {
    9. /**
    10. * 访问令牌
    11. */
    12. private String accessToken;
    13. /**
    14. * 访问令牌过期时间(毫秒)
    15. */
    16. private Long accessTokenExpire;
    17. /**
    18. * 刷新令牌
    19. */
    20. private String refreshToken;
    21. /**
    22. * 刷新令牌过期时间(毫秒)
    23. */
    24. private Long refreshExpire;
    25. public String getAccessToken() {
    26. return accessToken;
    27. }
    28. public void setAccessToken(String accessToken) {
    29. this.accessToken = accessToken;
    30. }
    31. public Long getAccessTokenExpire() {
    32. return accessTokenExpire;
    33. }
    34. public void setAccessTokenExpire(Long accessTokenExpire) {
    35. this.accessTokenExpire = accessTokenExpire;
    36. }
    37. public String getRefreshToken() {
    38. return refreshToken;
    39. }
    40. public void setRefreshToken(String refreshToken) {
    41. this.refreshToken = refreshToken;
    42. }
    43. public Long getRefreshExpire() {
    44. return refreshExpire;
    45. }
    46. public void setRefreshExpire(Long refreshExpire) {
    47. this.refreshExpire = refreshExpire;
    48. }
    49. }

    我们构架一个数据类在内存中存放用户数据,实际上存取数据库也应该有一个ORM类。

    1. package com.chris.demo.domain;
    2. /**
    3. * @author Chris Chan
    4. * Create On 2022/11/23 16:03
    5. * Use for:
    6. * Explain:
    7. */
    8. public class UserModel {
    9. private String username;
    10. private String password;
    11. private String authorities;
    12. public String getUsername() {
    13. return username;
    14. }
    15. public void setUsername(String username) {
    16. this.username = username;
    17. }
    18. public String getPassword() {
    19. return password;
    20. }
    21. public void setPassword(String password) {
    22. this.password = password;
    23. }
    24. public String getAuthorities() {
    25. return authorities;
    26. }
    27. public void setAuthorities(String authorities) {
    28. this.authorities = authorities;
    29. }
    30. }

    三、设计JavaWebToken的工具

    1. package com.chris.demo.utils;
    2. import com.chris.demo.domain.JwtResult;
    3. import io.jsonwebtoken.Claims;
    4. import io.jsonwebtoken.Jwts;
    5. import io.jsonwebtoken.SignatureAlgorithm;
    6. import java.util.Date;
    7. import java.util.HashMap;
    8. import java.util.Map;
    9. /**
    10. * @author Chris Chan
    11. * Create On 2022/11/24 10:18
    12. * Use for: JWT工具
    13. * Explain:
    14. */
    15. public class JwtUtil {
    16. /**
    17. * 访问令牌过期时间(天)
    18. */
    19. private static int ACCESS_TOKEN_XPIRE_DAYS = 7;
    20. /**
    21. * 刷新令牌过期时间(天)
    22. */
    23. private static int REFRESH_TOKEN_EXPIRE_DAYS = 30;
    24. /**
    25. * 签名指纹 加密解密要一致
    26. */
    27. private static String SIGN_KEY = "NDHHKHJKFWHEUIFKK8384SDNJAFYQJ723HF7823F3BJ";
    28. /**
    29. * 通过用户名构建jwt
    30. *
    31. * @param username
    32. * @param authorties
    33. * @param expire
    34. * @return
    35. */
    36. public static String buildJwtByUsername(String username, String authorties, long expire) {
    37. //有效载荷
    38. Map claims = new HashMap<>();
    39. claims.put("username", username);
    40. claims.put("authorities", authorties);
    41. //过期时间
    42. Date now = new Date();
    43. Date expireTime = new Date(expire);
    44. return Jwts.builder()
    45. .setId("yyds")
    46. .setSubject("chris_jwt")
    47. .setClaims(claims)
    48. .setIssuedAt(now)
    49. .setExpiration(expireTime)
    50. .signWith(SignatureAlgorithm.HS512, SIGN_KEY)
    51. .compact();
    52. }
    53. /**
    54. * 从jwt中解析出用户名
    55. *
    56. * @param token
    57. * @return
    58. */
    59. public static String getUsernameFromJwt(String token) {
    60. Object obj = parseClaimsBody(token)
    61. .get("username");
    62. return String.valueOf(obj);
    63. }
    64. /**
    65. * 从jwt中解析出权限
    66. *
    67. * @param token
    68. * @return
    69. */
    70. public static String getAuthoritiesFromJwt(String token) {
    71. Object obj = parseClaimsBody(token)
    72. .get("authorities");
    73. return String.valueOf(obj);
    74. }
    75. private static Claims parseClaimsBody(String token) {
    76. return Jwts.parser()
    77. .setSigningKey(SIGN_KEY)
    78. .parseClaimsJws(token)
    79. .getBody();
    80. }
    81. /**
    82. * 构建登录结果JwtResult
    83. *
    84. * @param username
    85. * @param authorities
    86. * @return
    87. */
    88. public static JwtResult buildJwtResultByUsername(String username, String authorities) {
    89. Date now = new Date();
    90. JwtResult jwtResult = new JwtResult();
    91. long accessTokenExpire = now.getTime() + 3600000 * 24 * ACCESS_TOKEN_XPIRE_DAYS;
    92. long refreshTokenExpire = now.getTime() + 3600000 * 24 * REFRESH_TOKEN_EXPIRE_DAYS;
    93. jwtResult.setAccessToken(buildJwtByUsername(username, authorities, accessTokenExpire));
    94. jwtResult.setAccessTokenExpire(accessTokenExpire);
    95. jwtResult.setRefreshToken(buildJwtByUsername(username, authorities, refreshTokenExpire));
    96. jwtResult.setRefreshExpire(refreshTokenExpire);
    97. return jwtResult;
    98. }
    99. }

    工具中三个主要方法,构建jwt,解析用户名、权限列表和构建登录结果。其中解析方法适用于一般业务模块验证token使用的。可以分离出去,但是key一定要保持一致。

    四、正菜之一:访问过滤器。调用一般业务接口时需要检查是否携带令牌,格式是否正确,验证之后进行授权。

    1. package com.chris.demo.config;
    2. import com.chris.demo.utils.JwtUtil;
    3. import org.springframework.security.authentication.AuthenticationManager;
    4. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    5. import org.springframework.security.core.GrantedAuthority;
    6. import org.springframework.security.core.authority.AuthorityUtils;
    7. import org.springframework.security.core.context.SecurityContextHolder;
    8. import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
    9. import javax.servlet.FilterChain;
    10. import javax.servlet.ServletException;
    11. import javax.servlet.http.HttpServletRequest;
    12. import javax.servlet.http.HttpServletResponse;
    13. import java.io.IOException;
    14. import java.util.Arrays;
    15. import java.util.List;
    16. import java.util.stream.Collectors;
    17. /**
    18. * @author Chris Chan
    19. * Create On 2022/11/23 17:07
    20. * Use for:
    21. * Explain:
    22. */
    23. public class AccessFilter extends BasicAuthenticationFilter {
    24. private static List passList;
    25. public static void setPassList(List passList) {
    26. AccessFilter.passList = passList;
    27. }
    28. public AccessFilter(AuthenticationManager authenticationManager, List passList) {
    29. super(authenticationManager);
    30. AccessFilter.passList = passList;
    31. }
    32. /**
    33. * 传入白名单和用户服务
    34. *
    35. * @param authenticationManager
    36. * @param passes
    37. */
    38. public AccessFilter(AuthenticationManager authenticationManager, String... passes) {
    39. super(authenticationManager);
    40. AccessFilter.passList = Arrays.stream(passes).collect(Collectors.toList());
    41. }
    42. @Override
    43. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    44. throws IOException, ServletException {
    45. //白名单放行
    46. String requestURI = request.getRequestURI();
    47. if (passList.contains(requestURI)) {
    48. chain.doFilter(request, response);
    49. return;
    50. }
    51. //读取Authorization
    52. String authorization = request.getHeader("Authorization");
    53. if (null == authorization) {
    54. throw new RuntimeException("没有发现令牌");
    55. }
    56. //格式校验
    57. if (!authorization.startsWith("Bearer ")) {
    58. throw new RuntimeException("令牌格式错误");
    59. }
    60. //取得token
    61. String jwt = authorization.replace("Bearer ", "");
    62. //todo 尝试解析jwt,获取用户名,有异常抛出,
    63. String username = JwtUtil.getUsernameFromJwt(jwt);
    64. String authorities = JwtUtil.getAuthoritiesFromJwt(jwt);
    65. //构建权限token,放入上下文
    66. List authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(authorities);
    67. UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorityList);
    68. SecurityContextHolder.getContext().setAuthentication(token);
    69. chain.doFilter(request, response);
    70. }
    71. }

    五、正菜之二:Security配置文件

    1. package com.chris.demo.config;
    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.method.configuration.EnableGlobalMethodSecurity;
    6. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    7. import org.springframework.security.config.annotation.web.builders.WebSecurity;
    8. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    9. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    10. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    11. import org.springframework.security.crypto.password.PasswordEncoder;
    12. /**
    13. * @author Chris Chan
    14. * Create On 2022/11/23 16:04
    15. * Use for:
    16. * Explain:
    17. * @link . https://blog.csdn.net/weixin_46684099/article/details/117434577
    18. * @link . https://blog.csdn.net/chihaihai/article/details/104678864
    19. */
    20. @EnableWebSecurity
    21. @EnableGlobalMethodSecurity(jsr250Enabled = true)
    22. @Configuration
    23. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    24. @Override
    25. public void configure(WebSecurity web) throws Exception {
    26. web
    27. .ignoring()
    28. .mvcMatchers("/");
    29. }
    30. @Override
    31. protected void configure(HttpSecurity http) throws Exception {
    32. //通关白名单
    33. String[] passes = {
    34. "/api/user/reg",
    35. "/api/user/login"
    36. };
    37. http
    38. .authorizeRequests()
    39. .antMatchers(passes).permitAll()
    40. .anyRequest().authenticated()
    41. .and()
    42. .cors()
    43. .and()
    44. .csrf().disable()
    45. .addFilter(new AccessFilter(authenticationManager(), passes));
    46. }
    47. @Bean
    48. @Override
    49. protected AuthenticationManager authenticationManager() throws Exception {
    50. return super.authenticationManager();
    51. }
    52. @Bean
    53. public PasswordEncoder passwordEncoder() {
    54. return new BCryptPasswordEncoder();
    55. }
    56. }

    核心在第二个configre方法,没有配置form和basic登录,只是对某些接口放行。包含一个访问过滤器设置。过滤器设置的白名单跟此处配置的完全授权的白名单没有必然的关系。不过一般是一致的。他们作用于两个过滤器,要都能通过才行。

    六、模型文件:模拟数据管理类,业务处理类,接口类等

    1. package com.chris.demo.dao;
    2. import com.chris.demo.domain.UserModel;
    3. import org.springframework.stereotype.Repository;
    4. import java.util.concurrent.ConcurrentHashMap;
    5. /**
    6. * @author Chris Chan
    7. * Create On 2022/11/24 9:57
    8. * Use for: 模拟数据库管理用户
    9. * Explain:
    10. */
    11. @Repository
    12. public class UserReporitory {
    13. private static ConcurrentHashMap userMap = new ConcurrentHashMap<>();
    14. /**
    15. * 保存用户
    16. * @param userModel
    17. */
    18. public void save(UserModel userModel) {
    19. userMap.put(userModel.getUsername(), userModel);
    20. }
    21. /**
    22. * 获取用户信息
    23. *
    24. * @param username
    25. * @return
    26. */
    27. public UserModel findByUsername(String username) {
    28. return userMap.get(username);
    29. }
    30. }
    1. package com.chris.demo.service;
    2. import com.chris.demo.dao.UserReporitory;
    3. import com.chris.demo.domain.JwtResult;
    4. import com.chris.demo.domain.UserModel;
    5. import com.chris.demo.utils.JwtUtil;
    6. import org.springframework.beans.factory.annotation.Autowired;
    7. import org.springframework.security.core.userdetails.UsernameNotFoundException;
    8. import org.springframework.security.crypto.password.PasswordEncoder;
    9. import org.springframework.stereotype.Service;
    10. import javax.security.auth.login.CredentialException;
    11. /**
    12. * @author Chris Chan
    13. * Create On 2022/11/23 16:25
    14. * Use for:
    15. * Explain:
    16. */
    17. @Service
    18. public class UserService {
    19. @Autowired
    20. PasswordEncoder passwordEncoder;
    21. @Autowired
    22. UserReporitory userReporitory;
    23. /**
    24. * 注册
    25. *
    26. * @param username
    27. * @param password
    28. */
    29. public void reg(String username, String password) {
    30. reg(username, password, "USER");
    31. }
    32. /**
    33. * 注册
    34. *
    35. * @param username
    36. * @param password
    37. */
    38. public void reg(String username, String password, String roles) {
    39. UserModel userModel = new UserModel();
    40. userModel.setUsername(username);
    41. userModel.setPassword(passwordEncoder.encode(password));
    42. userModel.setAuthorities(roles);
    43. userReporitory.save(userModel);
    44. }
    45. /**
    46. * 登录
    47. *
    48. * @param username
    49. * @param password
    50. * @return
    51. */
    52. public JwtResult login(String username, String password) {
    53. //检查用户是否存在
    54. UserModel userModel = userReporitory.findByUsername(username);
    55. if (null == userModel) {
    56. throw new UsernameNotFoundException("用户不存在");
    57. }
    58. //匹配密码
    59. String passwordEnc = userModel.getPassword();
    60. if (!passwordEncoder.matches(password, passwordEnc)) {
    61. try {
    62. throw new CredentialException("密码错误");
    63. } catch (CredentialException e) {
    64. e.printStackTrace();
    65. }
    66. return null;
    67. }
    68. return JwtUtil.buildJwtResultByUsername(username, userModel.getAuthorities());
    69. }
    70. }

    这个UserService并没有去实现Securty的UserDetailService接口,因为逻辑完全是由我们自己处理的。

    1. package com.chris.demo.web;
    2. import com.chris.demo.domain.JwtResult;
    3. import com.chris.demo.service.UserService;
    4. import org.springframework.beans.factory.annotation.Autowired;
    5. import org.springframework.web.bind.annotation.GetMapping;
    6. import org.springframework.web.bind.annotation.RequestMapping;
    7. import org.springframework.web.bind.annotation.RestController;
    8. /**
    9. * @author Chris Chan
    10. * Create On 2022/11/23 16:09
    11. * Use for: 用户接口
    12. * Explain:
    13. */
    14. @RestController
    15. @RequestMapping("api/user")
    16. public class UserController {
    17. @Autowired
    18. UserService accountService;
    19. /**
    20. * 模拟注册
    21. *
    22. * @param username
    23. * @param password
    24. * @return
    25. */
    26. @GetMapping("reg")
    27. public String reg(String username, String password, String roles) {
    28. accountService.reg(username, password, roles);
    29. return "reg success";
    30. }
    31. /**
    32. * 模拟登录
    33. *
    34. * @param username
    35. * @param password
    36. * @return
    37. */
    38. @GetMapping("login")
    39. public JwtResult login(String username, String password) {
    40. return accountService.login(username, password);
    41. }
    42. }
    1. package com.chris.demo.web;
    2. import org.springframework.security.access.annotation.Secured;
    3. import org.springframework.web.bind.annotation.GetMapping;
    4. import org.springframework.web.bind.annotation.RequestMapping;
    5. import org.springframework.web.bind.annotation.RestController;
    6. import javax.annotation.security.RolesAllowed;
    7. /**
    8. * @author Chris Chan
    9. * Create On 2022/11/23 16:41
    10. * Use for: 业务接口
    11. * Explain:
    12. */
    13. @RestController
    14. @RequestMapping("api/biz")
    15. public class BizController {
    16. /**
    17. * 模拟业务调用
    18. *
    19. * @return
    20. */
    21. @GetMapping("test")
    22. public String test() {
    23. return "test success.";
    24. }
    25. /**
    26. * 权限验证
    27. * @return
    28. */
    29. @RolesAllowed({"ADMIN"})
    30. @GetMapping("test2")
    31. public String test2() {
    32. return "test2 success.";
    33. }
    34. }

    两个接口文件,一个设计为UAA模块,一个设计为业务模块。业务接口有一个是测试权限的。

    测试结果:

    1.先注册一个用户:

     给定的权限是ROLE_USER,前面的ROLE_前缀是必须要加的,这是jsr250的权限校验规范要求的。

    2.登录,获得jwt

    3.将accessToken填入Authorization的Bearer Token模式,调用没有权限限制的接口

     

    4. 调用有权限限制的接口,报403

     

     5.另外注册一个用户权限设置为ROLE_ADMIN,登录获取accessToken,重新调用限权接口

    6.前端调用接口的时候实在header中增加一个key为Authorization的数据,其值为accessToken加上bearer+一个空格的前缀,就像这样

     

    完美!! 

  • 相关阅读:
    第 4 章 串(串的堆分配存储实现)
    java毕业设计校园管理系统mybatis+源码+调试部署+系统+数据库+lw
    引入echarts.js操作步骤
    AI带你省钱旅游!精准预测民宿房源价格! ⛵
    【Node.js】深度解析常用核心模块-path模块
    【MySQL】表的基本操作
    域名解析异常如何解决?快解析轻松实现动态域名解析
    vue项目身份认证,vuex,token
    Elasticsearch 开放 inference API 增加了对 Azure OpenAI 嵌入的支持
    input上传图片,并预览
  • 原文地址:https://blog.csdn.net/xxkalychen/article/details/128015285