实战篇:Security+JWT组合拳 | 附源码

绝地灬酷狼 2023-10-03 09:55 12阅读 0赞

Good morning, everyone!

之前我们已经说过用Shiro和JWT来实现身份认证和用户授权,今天我们再来说一下Security和JWT的组合拳。

简介

先赘述一下身份认证和用户授权:

  • 用户认证(Authentication):系统通过校验用户提供的用户名和密码来验证该用户是否为系统中的合法主体,即是否可以访问该系统;
  • 用户授权(Authorization):系统为用户分配不同的角色,以获取对应的权限,即验证该用户是否有权限执行该操作;

Web应用的安全性包括用户认证和用户授权两个部分,而Spring Security(以下简称Security)基于Spring框架,正好可以完整解决该问题。

它的真正强大之处在于它可以轻松扩展以满足自定义要求。

原理

Security可以看做是由一组filter过滤器链组成的权限认证。它的整个工作流程如下所示:
d13ed9f0fec8d59df10effad5e932a01.png
图中绿色认证方式是可以配置的,橘黄色和蓝色的位置不可更改:

  • FilterSecurityInterceptor:最后的过滤器,它会决定当前的请求可不可以访问Controller
  • ExceptionTranslationFilter:异常过滤器,接收到异常消息时会引导用户进行认证;

实战

项目准备

我们使用Spring Boot框架来集成。

1.pom文件引入的依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-web</artifactId>
  8. <exclusions>
  9. <exclusion>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-tomcat</artifactId>
  12. </exclusion>
  13. </exclusions>
  14. </dependency>
  15. <dependency>
  16. <groupId>org.springframework.boot</groupId>
  17. <artifactId>spring-boot-starter-undertow</artifactId>
  18. </dependency>
  19. <dependency>
  20. <groupId>mysql</groupId>
  21. <artifactId>mysql-connector-java</artifactId>
  22. </dependency>
  23. <dependency>
  24. <groupId>com.baomidou</groupId>
  25. <artifactId>mybatis-plus-boot-starter</artifactId>
  26. <version>3.4.0</version>
  27. </dependency>
  28. <dependency>
  29. <groupId>org.projectlombok</groupId>
  30. <artifactId>lombok</artifactId>
  31. </dependency>
  32. <!-- 阿里JSON解析器 -->
  33. <dependency>
  34. <groupId>com.alibaba</groupId>
  35. <artifactId>fastjson</artifactId>
  36. <version>1.2.74</version>
  37. </dependency>
  38. <dependency>
  39. <groupId>joda-time</groupId>
  40. <artifactId>joda-time</artifactId>
  41. <version>2.10.6</version>
  42. </dependency>
  43. <dependency>
  44. <groupId>org.springframework.boot</groupId>
  45. <artifactId>spring-boot-starter-test</artifactId>
  46. </dependency>

2.application.yml配置

  1. spring:
  2. application:
  3. name: securityjwt
  4. datasource:
  5. driver-class-name: com.mysql.cj.jdbc.Driver
  6. url: jdbc:mysql://127.0.0.1:3306/cheetah?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
  7. username: root
  8. password: 123456
  9. server:
  10. port: 8080
  11. mybatis:
  12. mapper-locations: classpath:mapper/*.xml
  13. type-aliases-package: com.itcheetah.securityjwt.entity
  14. configuration:
  15. map-underscore-to-camel-case: true
  16. rsa:
  17. key:
  18. pubKeyFile: C:\Users\Desktop\jwt\id_key_rsa.pub
  19. priKeyFile: C:\Users\Desktop\jwt\id_key_rsa

3.SQL文件

  1. /**
  2. * sys_user_info
  3. **/
  4. SET NAMES utf8mb4;
  5. SET FOREIGN_KEY_CHECKS = 0;
  6. -- ----------------------------
  7. -- Table structure for sys_user_info
  8. -- ----------------------------
  9. DROP TABLE IF EXISTS `sys_user_info`;
  10. CREATE TABLE `sys_user_info` (
  11. `id` bigint(20) NOT NULL AUTO_INCREMENT,
  12. `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  13. `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  14. PRIMARY KEY (`id`) USING BTREE
  15. ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
  16. SET FOREIGN_KEY_CHECKS = 1;
  17. /**
  18. * product_info
  19. **/
  20. SET NAMES utf8mb4;
  21. SET FOREIGN_KEY_CHECKS = 0;
  22. -- ----------------------------
  23. -- Table structure for product_info
  24. -- ----------------------------
  25. DROP TABLE IF EXISTS `product_info`;
  26. CREATE TABLE `product_info` (
  27. `id` bigint(20) NOT NULL AUTO_INCREMENT,
  28. `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  29. `price` decimal(10, 4) NULL DEFAULT NULL,
  30. `create_date` datetime(0) NULL DEFAULT NULL,
  31. `update_date` datetime(0) NULL DEFAULT NULL,
  32. PRIMARY KEY (`id`) USING BTREE
  33. ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
  34. SET FOREIGN_KEY_CHECKS = 1;

引入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>
  5. <!--Token生成与解析-->
  6. <dependency>
  7. <groupId>io.jsonwebtoken</groupId>
  8. <artifactId>jjwt</artifactId>
  9. <version>0.9.1</version>
  10. </dependency>

引入之后启动项目,会有如图所示:
f3657c4ac1e6f76abaf9fe05d4079914.png
其中用户名为user,密码为上图中的字符串。

SecurityConfig类

  1. //开启全局方法安全性
  2. @EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled=true)
  3. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  4. //认证失败处理类
  5. @Autowired
  6. private AuthenticationEntryPointImpl unauthorizedHandler;
  7. //提供公钥私钥的配置类
  8. @Autowired
  9. private RsaKeyProperties prop;
  10. @Autowired
  11. private UserInfoService userInfoService;
  12. @Override
  13. protected void configure(HttpSecurity httpSecurity) throws Exception {
  14. httpSecurity
  15. // CSRF禁用,因为不使用session
  16. .csrf().disable()
  17. // 认证失败处理类
  18. .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
  19. // 基于token,所以不需要session
  20. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
  21. // 过滤请求
  22. .authorizeRequests()
  23. .antMatchers(
  24. HttpMethod.GET,
  25. "/*.html",
  26. "/**/*.html",
  27. "/**/*.css",
  28. "/**/*.js"
  29. ).permitAll()
  30. // 除上面外的所有请求全部需要鉴权认证
  31. .anyRequest().authenticated()
  32. .and()
  33. .headers().frameOptions().disable();
  34. // 添加JWT filter
  35. httpSecurity.addFilter(new TokenLoginFilter(super.authenticationManager(), prop))
  36. .addFilter(new TokenVerifyFilter(super.authenticationManager(), prop));
  37. }
  38. //指定认证对象的来源
  39. public void configure(AuthenticationManagerBuilder auth) throws Exception {
  40. auth.userDetailsService(userInfoService)
  41. //从前端传递过来的密码就会被加密,所以从数据库
  42. //查询到的密码必须是经过加密的,而这个过程都是
  43. //在用户注册的时候进行加密的。
  44. .passwordEncoder(passwordEncoder());
  45. }
  46. //密码加密
  47. @Bean
  48. public BCryptPasswordEncoder passwordEncoder(){
  49. return new BCryptPasswordEncoder();
  50. }
  51. }

拦截规则

  • anyRequest:匹配所有请求路径
  • accessSpringEl表达式结果为true时可以访问
  • anonymous:匿名可以访问
  • `denyAll:用户不能访问
  • fullyAuthenticated:用户完全认证可以访问(非remember-me下自动登录)
  • hasAnyAuthority:如果有参数,参数表示权限,则其中任何一个权限可以访问
  • hasAnyRole:如果有参数,参数表示角色,则其中任何一个角色可以访问
  • hasAuthority:如果有参数,参数表示权限,则其权限可以访问
  • hasIpAddress:如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
  • hasRole:如果有参数,参数表示角色,则其角色可以访问
  • permitAll:用户可以任意访问
  • rememberMe:允许通过remember-me登录的用户访问
  • authenticated:用户登录后可访问
认证失败处理类
  1. /**
  2. * 返回未授权
  3. */
  4. @Component
  5. public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
  6. private static final long serialVersionUID = -8970718410437077606L;
  7. @Override
  8. public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
  9. throws IOException {
  10. int code = HttpStatus.UNAUTHORIZED;
  11. String msg = "认证失败,无法访问系统资源,请先登陆";
  12. ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
  13. }
  14. }

认证流程

自定义认证过滤器
  1. public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
  2. private AuthenticationManager authenticationManager;
  3. private RsaKeyProperties prop;
  4. public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
  5. this.authenticationManager = authenticationManager;
  6. this.prop = prop;
  7. }
  8. /**
  9. * @author cheetah
  10. * @description 登陆验证
  11. * @date 2021/6/28 16:17
  12. * @Param [request, response]
  13. * @return org.springframework.security.core.Authentication
  14. **/
  15. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  16. try {
  17. UserPojo sysUser = new ObjectMapper().readValue(request.getInputStream(), UserPojo.class);
  18. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());
  19. return authenticationManager.authenticate(authRequest);
  20. }catch (Exception e){
  21. try {
  22. response.setContentType("application/json;charset=utf-8");
  23. response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
  24. PrintWriter out = response.getWriter();
  25. Map resultMap = new HashMap();
  26. resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
  27. resultMap.put("msg", "用户名或密码错误!");
  28. out.write(new ObjectMapper().writeValueAsString(resultMap));
  29. out.flush();
  30. out.close();
  31. }catch (Exception outEx){
  32. outEx.printStackTrace();
  33. }
  34. throw new RuntimeException(e);
  35. }
  36. }
  37. /**
  38. * @author cheetah
  39. * @description 登陆成功回调
  40. * @date 2021/6/28 16:17
  41. * @Param [request, response, chain, authResult]
  42. * @return void
  43. **/
  44. public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
  45. UserPojo user = new UserPojo();
  46. user.setUsername(authResult.getName());
  47. user.setRoles((List<RolePojo>)authResult.getAuthorities());
  48. //通过私钥进行加密:token有效期一天
  49. String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24 * 60);
  50. response.addHeader("Authorization", "Bearer "+token);
  51. try {
  52. response.setContentType("application/json;charset=utf-8");
  53. response.setStatus(HttpServletResponse.SC_OK);
  54. PrintWriter out = response.getWriter();
  55. Map resultMap = new HashMap();
  56. resultMap.put("code", HttpServletResponse.SC_OK);
  57. resultMap.put("msg", "认证通过!");
  58. resultMap.put("token", token);
  59. out.write(new ObjectMapper().writeValueAsString(resultMap));
  60. out.flush();
  61. out.close();
  62. }catch (Exception outEx){
  63. outEx.printStackTrace();
  64. }
  65. }
  66. }
流程

Security默认登录路径为/login,当我们调用该接口时,它会调用上边的attemptAuthentication方法;
74b88fa270d61bd26cd62414cd4d096b.png
b85a345b378631ec784d225790d7a3b6.png
157b99da015195c5f062dbd35f6cdd98.png
4c6c3c0cf0c1bdfbabdab39fcebe73a5.png
所以我们要自定义UserInfoService继承UserDetailsService实现loadUserByUsername方法;

  1. public interface UserInfoService extends UserDetailsService {
  2. }
  3. @Service
  4. @Transactional
  5. public class UserInfoServiceImpl implements UserInfoService {
  6. @Autowired
  7. private SysUserInfoMapper userInfoMapper;
  8. @Override
  9. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  10. UserPojo user = userInfoMapper.queryByUserName(username);
  11. return user;
  12. }
  13. }

其中的loadUserByUsername返回的是UserDetails类型,所以UserPojo继承UserDetails

  1. @Data
  2. public class UserPojo implements UserDetails {
  3. private Integer id;
  4. private String username;
  5. private String password;
  6. private Integer status;
  7. private List<RolePojo> roles;
  8. @JsonIgnore
  9. @Override
  10. public Collection<? extends GrantedAuthority> getAuthorities() {
  11. //理想型返回 admin 权限,可自已处理这块
  12. List<SimpleGrantedAuthority> auth = new ArrayList<>();
  13. auth.add(new SimpleGrantedAuthority("ADMIN"));
  14. return auth;
  15. }
  16. @Override
  17. public String getPassword() {
  18. return this.password;
  19. }
  20. @Override
  21. public String getUsername() {
  22. return this.username;
  23. }
  24. /**
  25. * 账户是否过期
  26. **/
  27. @JsonIgnore
  28. @Override
  29. public boolean isAccountNonExpired() {
  30. return true;
  31. }
  32. /**
  33. * 是否禁用
  34. */
  35. @JsonIgnore
  36. @Override
  37. public boolean isAccountNonLocked() {
  38. return true;
  39. }
  40. /**
  41. * 密码是否过期
  42. */
  43. @JsonIgnore
  44. @Override
  45. public boolean isCredentialsNonExpired() {
  46. return true;
  47. }
  48. /**
  49. * 是否启用
  50. */
  51. @JsonIgnore
  52. @Override
  53. public boolean isEnabled() {
  54. return true;
  55. }
  56. }

当认证通过之后会在SecurityContext中设置Authentication对象,回调调用successfulAuthentication方法返回token信息,
7db8e465adb8c86aa6ce8aacb22f1943.png

整体流程图如下

bdbe7accbb4b1637981f7a1b977534de.png

鉴权流程

自定义token过滤器
  1. public class TokenVerifyFilter extends BasicAuthenticationFilter {
  2. private RsaKeyProperties prop;
  3. public TokenVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
  4. super(authenticationManager);
  5. this.prop = prop;
  6. }
  7. public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
  8. String header = request.getHeader("Authorization");
  9. if (header == null || !header.startsWith("Bearer ")) {
  10. //如果携带错误的token,则给用户提示请登录!
  11. chain.doFilter(request, response);
  12. } else {
  13. //如果携带了正确格式的token要先得到token
  14. String token = header.replace("Bearer ", "");
  15. //通过公钥进行解密:验证tken是否正确
  16. Payload<UserPojo> payload = JwtUtils.getInfoFromToken(token, prop.getPublicKey(), UserPojo.class);
  17. UserPojo user = payload.getUserInfo();
  18. if(user!=null){
  19. UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());
  20. //将认证信息存到安全上下文中
  21. SecurityContextHolder.getContext().setAuthentication(authResult);
  22. chain.doFilter(request, response);
  23. }
  24. }
  25. }
  26. }

当我们访问时需要在header中携带token信息
90b47a67ad87b05611f4b0adbbd0c288.png

至于关于文中JWT生成tokenRSA生成公钥、私钥的部分,可在源码中查看,回复“sjwt”可获取完整源码呦!

以上就是今天的全部内容了,如果你有不同的意见或者更好的idea,欢迎联系阿Q,添加阿Q可以加入技术交流群参与讨论呦!

后台留言领取 java 干货资料:学习笔记与大厂面试题

发表评论

表情:
评论列表 (有 0 条评论,12人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Redis未授权+CVE-2019-0708组合利用

    0x01 简介 本次测试为实战测试,测试环境是授权项目中的一部分,敏感信息内容已做打码处理,仅供讨论学习。请大家测试的时候,务必取得授权。 拿到授权项目的时候,客户只给