经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring Boot » 查看文章
基于Springboot集成security、oauth2实现认证鉴权、资源管理
来源:cnblogs  作者:丰哥坑到  时间:2019/4/23 8:58:36  对本文有异议

  

1、Oauth2简介

  OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。

2、Oauth2服务器

  • 授权服务器 Authorization Service.
  • 资源服务器 Resource Service.

 授权服务器

  授权服务器,即服务提供商专门用来处理认证的服务器在这里简单说一下,主要的功能;

  1、通过请求获得令牌(Token),默认的URL是/oauth/token.

   2、根据令牌(Token)获取相应的权限.

资源服务器

  资源服务器托管了受保护的用户账号信息,并且对接口资源进行用户权限分配及管理,简单的说,就是某个接口(/user/add),我限制只能持有管理员权限的用户才能访问,那么普通用户就没有访问的权限。

 

以下摘自百度百科图:

 

3、Demo实战加代码详解

      前面我是简单地介绍了一下oauth2的一些基本概念,关于oauth2的深入介绍,可以去搜索更多其它相关oauth2的博文,在这里推荐一篇前辈的博文https://www.cnblogs.com/Wddpct/p/8976480.html,里面有详细的oauth2介绍,包括原理、实现流程等都讲得比较详细。我的课题,是主要是以实战为主,理论的东西我不想介绍太多, 这里是我个人去根据自己的业务需求去改造的,存在很多可优化的点,希望大家可以指出和给予我一些宝贵意见。

  接下来开始介绍我的代码流程吧! 

准备

 新建一个springboot项目,引入以下依赖。

 

  1. 1 <dependencies>
  2. 2 <dependency>
  3. 3 <groupId>org.springframework.boot</groupId>
  4. 4 <artifactId>spring-boot-starter</artifactId>
  5. 5 </dependency>
  6. 6 <dependency>
  7. 7 <groupId>org.springframework.boot</groupId>
  8. 8 <artifactId>spring-boot-starter-test</artifactId>
  9. 9 <scope>test</scope>
  10. 10 </dependency>
  11. 11
  12. 12 <!--web依赖-->
  13. 13 <dependency>
  14. 14 <groupId>org.springframework.boot</groupId>
  15. 15 <artifactId>spring-boot-starter-web</artifactId>
  16. 16 </dependency>
  17. 17
  18. 18 <!--redis依赖-->
  19. 19 <dependency>
  20. 20 <groupId>org.springframework.boot</groupId>
  21. 21 <artifactId>spring-boot-starter-data-redis</artifactId>
  22. 22 </dependency>
  23. 23
  24. 24 <!--sl4f日志框架-->
  25. 25 <dependency>
  26. 26 <groupId>org.projectlombok</groupId>
  27. 27 <artifactId>lombok</artifactId>
  28. 28 </dependency>
  29. 29
  30. 30 <!--security依赖-->
  31. 31 <dependency>
  32. 32 <groupId>org.springframework.boot</groupId>
  33. 33 <artifactId>spring-boot-starter-security</artifactId>
  34. 34 </dependency>
  35. 35 <!--oauth2依赖-->
  36. 36 <dependency>
  37. 37 <groupId>org.springframework.security.oauth</groupId>
  38. 38 <artifactId>spring-security-oauth2</artifactId>
  39. 39 <version>2.3.3.RELEASE</version>
  40. 40 </dependency>
  41. 41
  42. 42 <!--JPA数据库持久化-->
  43. 43 <dependency>
  44. 44 <groupId>mysql</groupId>
  45. 45 <artifactId>mysql-connector-java</artifactId>
  46. 46 <version>5.1.47</version>
  47. 47 <scope>runtime</scope>
  48. 48 </dependency>
  49. 49 <dependency>
  50. 50 <groupId>org.springframework.boot</groupId>
  51. 51 <artifactId>spring-boot-starter-data-jpa</artifactId>
  52. 52 </dependency>
  53. 53
  54. 54 <!--json工具-->
  55. 55 <dependency>
  56. 56 <groupId>com.alibaba</groupId>
  57. 57 <artifactId>fastjson</artifactId>
  58. 58 <version>1.2.47</version>
  59. 59 </dependency>
  60. 60 </dependencies>

 

 

 

 

项目目录结构

 

 

 

接口

这里我只编写了一个AuthController,里面基本所有关于用户管理及登录、注销的接口我都定义出来了。

AuthController代码如下:

  1. 1 package com.unionman.springbootsecurityauth2.controller;
  2. 2
  3. 3 import com.unionman.springbootsecurityauth2.dto.LoginUserDTO;
  4. 4 import com.unionman.springbootsecurityauth2.dto.UserDTO;
  5. 5 import com.unionman.springbootsecurityauth2.service.RoleService;
  6. 6 import com.unionman.springbootsecurityauth2.service.UserService;
  7. 7 import com.unionman.springbootsecurityauth2.utils.AssertUtils;
  8. 8 import com.unionman.springbootsecurityauth2.vo.ResponseVO;
  9. 9 import lombok.extern.slf4j.Slf4j;
  10. 10 import org.springframework.beans.factory.annotation.Autowired;
  11. 11 import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
  12. 12 import org.springframework.validation.annotation.Validated;
  13. 13 import org.springframework.web.bind.annotation.*;
  14. 14
  15. 15 import javax.validation.Valid;
  16. 16
  17. 17 /**
  18. 18 * @description 用户权限管理
  19. 19 * @author Zhifeng.Zeng
  20. 20 * @date 2019/4/19 13:58
  21. 21 */
  22. 22 @Slf4j
  23. 23 @Validated
  24. 24 @RestController
  25. 25 @RequestMapping("/auth/")
  26. 26 public class AuthController {
  27. 27
  28. 28 @Autowired
  29. 29 private UserService userService;
  30. 30
  31. 31 @Autowired
  32. 32 private RoleService roleService;
  33. 33
  34. 34 @Autowired
  35. 35 private RedisTokenStore redisTokenStore;
  36. 36
  37. 37 /**
  38. 38 * @description 添加用户
  39. 39 * @param userDTO
  40. 40 * @return
  41. 41 */
  42. 42 @PostMapping("user")
  43. 43 public ResponseVO add(@Valid @RequestBody UserDTO userDTO){
  44. 44 userService.addUser(userDTO);
  45. 45 return ResponseVO.success();
  46. 46 }
  47. 47
  48. 48 /**
  49. 49 * @description 删除用户
  50. 50 * @param id
  51. 51 * @return
  52. 52 */
  53. 53 @DeleteMapping("user/{id}")
  54. 54 public ResponseVO deleteUser(@PathVariable("id")Integer id){
  55. 55 userService.deleteUser(id);
  56. 56 return ResponseVO.success();
  57. 57 }
  58. 58
  59. 59 /**
  60. 60 * @descripiton 修改用户
  61. 61 * @param userDTO
  62. 62 * @return
  63. 63 */
  64. 64 @PutMapping("user")
  65. 65 public ResponseVO updateUser(@Valid @RequestBody UserDTO userDTO){
  66. 66 userService.updateUser(userDTO);
  67. 67 return ResponseVO.success();
  68. 68 }
  69. 69
  70. 70 /**
  71. 71 * @description 获取用户列表
  72. 72 * @return
  73. 73 */
  74. 74 @GetMapping("user")
  75. 75 public ResponseVO findAllUser(){
  76. 76 return userService.findAllUserVO();
  77. 77 }
  78. 78
  79. 79 /**
  80. 80 * @description 用户登录
  81. 81 * @param loginUserDTO
  82. 82 * @return
  83. 83 */
  84. 84 @PostMapping("user/login")
  85. 85 public ResponseVO login(LoginUserDTO loginUserDTO){
  86. 86 return userService.login(loginUserDTO);
  87. 87 }
  88. 88
  89. 89
  90. 90 /**
  91. 91 * @description 用户注销
  92. 92 * @param authorization
  93. 93 * @return
  94. 94 */
  95. 95 @GetMapping("user/logout")
  96. 96 public ResponseVO logout(@RequestHeader("Authorization") String authorization){
  97. 97 redisTokenStore.removeAccessToken(AssertUtils.extracteToken(authorization));
  98. 98 return ResponseVO.success();
  99. 99 }
  100. 100
  101. 101 /**
  102. 102 * @description 用户刷新Token
  103. 103 * @param refreshToken
  104. 104 * @return
  105. 105 */
  106. 106 @GetMapping("user/refresh/{refreshToken}")
  107. 107 public ResponseVO refresh(@PathVariable(value = "refreshToken") String refreshToken){
  108. 108 return userService.refreshToken(refreshToken);
  109. 109 }
  110. 110
  111. 111
  112. 112 /**
  113. 113 * @description 获取所有角色列表
  114. 114 * @return
  115. 115 */
  116. 116 @GetMapping("role")
  117. 117 public ResponseVO findAllRole(){
  118. 118 return roleService.findAllRoleVO();
  119. 119 }
  120. 120
  121. 121
  122. 122 }

  这里所有的接口功能,我都已经在业务代码里实现了,后面相关登录、注销、及刷新token的等接口的业务实现的内容我会贴出来。接下来我需要讲解的是关于oath2及security的详细配置。

注意一点:这里没有角色的增删改功能,只有获取角色列表功能,为了节省时间,我这里的角色列表是项目初始化阶段,直接生成的固定的两个角色,分别是ROLE_USER(普通用户)、ROLE_ADMIN(管理员);同时初始化一个默认的管理员。

 

springbootsecurityauth.sql脚本如下:

  1. 1 SET NAMES utf8;
  2. 2 SET FOREIGN_KEY_CHECKS = 0;
  3. 3 /**
  4. 4 初始化角色信息
  5. 5 */
  6. 6 CREATE TABLE IF NOT EXISTS `um_t_role`(
  7. 7 `id` INT(11) PRIMARY KEY AUTO_INCREMENT ,
  8. 8 `description` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  9. 9 `created_time` BIGINT(20) NOT NULL,
  10. 10 `name` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  11. 11 `role` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL
  12. 12 );
  13. 13 INSERT IGNORE INTO `um_t_role`(id,`name`,description,created_time,created_user,role) VALUES(1,'管理员','管理员拥有所有接口操作权限',UNIX_TIMESTAMP(NOW()),'ADMIN'),(2,'普通用户','普通拥有查看用户列表与修改密码权限,不具备对用户增删改权限',UNIX_TIMESTAMP(NOW()),'USER');
  14. 14
  15. 15 /**
  16. 16 初始化一个默认管理员
  17. 17 */
  18. 18 CREATE TABLE IF NOT EXISTS `um_t_user`(
  19. 19 `id` INT(11) PRIMARY KEY AUTO_INCREMENT ,
  20. 20 `account` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  21. 21 `description` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  22. 22 `password` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  23. 23 `name` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL
  24. 24 );
  25. 25 INSERT IGNORE INTO `um_t_user`(id,account,`password`,`name`,description) VALUES(1,'admin','123456','小小丰','系统默认管理员');
  26. 26
  27. 27 /**
  28. 28 关联表赋值
  29. 29 */
  30. 30 CREATE TABLE IF NOT EXISTS `um_t_role_user`(
  31. 31 `role_id` INT(11),
  32. 32 `user_id` INT(11)
  33. 33 );
  34. 34 INSERT IGNORE INTO `um_t_role_user`(role_id,user_id)VALUES(1,1);

 

配置

application.yml文件:

  1. 1 server:
  2. 2 port: 8080
  3. 3 spring:
  4. 4 # mysql 配置
  5. 5 datasource:
  6. 6 url: jdbc:mysql://localhost:3306/auth_test?useUnicode=true&characterEncoding=UTF-8&useSSL=false
  7. 7 username: root
  8. 8 password: 123456
  9. 9 schema: classpath:springbootsecurityauth.sql
  10. 10 sql-script-encoding: utf-8
  11. 11 initialization-mode: always
  12. 12 driver-class-name: com.mysql.jdbc.Driver
  13. 13 # 初始化大小,最小,最大
  14. 14 initialSize: 1
  15. 15 minIdle: 3
  16. 16 maxActive: 20
  17. 17 # 配置获取连接等待超时的时间
  18. 18 maxWait: 60000
  19. 19 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  20. 20 timeBetweenEvictionRunsMillis: 60000
  21. 21 # 配置一个连接在池中最小生存的时间,单位是毫秒
  22. 22 minEvictableIdleTimeMillis: 30000
  23. 23 validationQuery: select 'x'
  24. 24 testWhileIdle: true
  25. 25 testOnBorrow: false
  26. 26 testOnReturn: false
  27. 27 # 打开PSCache,并且指定每个连接上PSCache的大小
  28. 28 poolPreparedStatements: true
  29. 29 maxPoolPreparedStatementPerConnectionSize: 20
  30. 30 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
  31. 31 filters: stat,wall,slf4j
  32. 32 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
  33. 33 connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
  34. 34 #redis 配置
  35. 35 redis:
  36. 36 open: true # 是否开启redis缓存 true开启 false关闭
  37. 37 database: 1
  38. 38 host: localhost
  39. 39 port: 6379
  40. 40 timeout: 5000s # 连接超时时长(毫秒)
  41. 41 jedis:
  42. 42 pool:
  43. 43 max-active: 8 #连接池最大连接数(使用负值表示没有限制)
  44. 44 max-idle: 8 #连接池中的最大空闲连接
  45. 45 max-wait: -1s #连接池最大阻塞等待时间(使用负值表示没有限制)
  46. 46 min-idle: 0 #连接池中的最小空闲连接
  47. 47
  48. 48 # jpa 配置
  49. 49 jpa:
  50. 50 database: mysql
  51. 51 show-sql: false
  52. 52 hibernate:
  53. 53 ddl-auto: update
  54. 54 properties:
  55. 55 hibernate:
  56. 56 dialect: org.hibernate.dialect.MySQL5Dialect

 

资源服务器与授权服务器

编写类Oauth2Config,实现资源服务器与授权服务器,这里的资源服务器与授权服务器以内部类的形式实现。

Oauth2Config代码如下:

 

  1. 1 package com.unionman.springbootsecurityauth2.config;
  2. 2
  3. 3 import com.unionman.springbootsecurityauth2.handler.CustomAuthExceptionHandler;
  4. 4 import org.springframework.beans.factory.annotation.Autowired;
  5. 5 import org.springframework.context.annotation.Bean;
  6. 6 import org.springframework.context.annotation.Configuration;
  7. 7 import org.springframework.data.redis.connection.RedisConnectionFactory;
  8. 8 import org.springframework.http.HttpMethod;
  9. 9 import org.springframework.security.authentication.AuthenticationManager;
  10. 10 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  11. 11 import org.springframework.security.config.http.SessionCreationPolicy;
  12. 12 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  13. 13 import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
  14. 14 import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
  15. 15 import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
  16. 16 import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
  17. 17 import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
  18. 18 import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
  19. 19 import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
  20. 20 import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
  21. 21 import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
  22. 22 import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
  23. 23
  24. 24 import java.util.concurrent.TimeUnit;
  25. 25
  26. 26
  27. 27
  28. 28 /**
  29. 29 * @author Zhifeng.Zeng
  30. 30 * @description OAuth2服务器配置
  31. 31 */
  32. 32 @Configuration
  33. 33 public class OAuth2Config {
  34. 34
  35. 35 public static final String ROLE_ADMIN = "ADMIN";
  36. 36 //访问客户端密钥
  37. 37 public static final String CLIENT_SECRET = "123456";
  38. 38 //访问客户端ID
  39. 39 public static final String CLIENT_ID ="client_1";
  40. 40 //鉴权模式
  41. 41 public static final String GRANT_TYPE[] = {"password","refresh_token"};
  42. 42
  43. 43 /**
  44. 44 * @description 资源服务器
  45. 45 */
  46. 46 @Configuration
  47. 47 @EnableResourceServer
  48. 48 protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
  49. 49
  50. 50 @Autowired
  51. 51 private CustomAuthExceptionHandler customAuthExceptionHandler;
  52. 52
  53. 53 @Override
  54. 54 public void configure(ResourceServerSecurityConfigurer resources) {
  55. 55 resources.stateless(false)
  56. 56 .accessDeniedHandler(customAuthExceptionHandler)
  57. 57 .authenticationEntryPoint(customAuthExceptionHandler);
  58. 58 }
  59. 59
  60. 60 @Override
  61. 61 public void configure(HttpSecurity http) throws Exception {
  62. 62 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
  63. 63 .and()
  64. 64 //请求权限配置
  65. 65 .authorizeRequests()
  66. 66 //下边的路径放行,不需要经过认证
  67. 67 .antMatchers("/oauth/*", "/auth/user/login").permitAll()
  68. 68 //OPTIONS请求不需要鉴权
  69. 69 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
  70. 70 //用户的增删改接口只允许管理员访问
  71. 71 .antMatchers(HttpMethod.POST, "/auth/user").hasAnyAuthority(ROLE_ADMIN)
  72. 72 .antMatchers(HttpMethod.PUT, "/auth/user").hasAnyAuthority(ROLE_ADMIN)
  73. 73 .antMatchers(HttpMethod.DELETE, "/auth/user").hasAnyAuthority(ROLE_ADMIN)
  74. 74 //获取角色 权限列表接口只允许系统管理员及高级用户访问
  75. 75 .antMatchers(HttpMethod.GET, "/auth/role").hasAnyAuthority(ROLE_ADMIN)
  76. 76 //其余接口没有角色限制,但需要经过认证,只要携带token就可以放行
  77. 77 .anyRequest()
  78. 78 .authenticated();
  79. 79
  80. 80 }
  81. 81 }
  82. 82
  83. 83 /**
  84. 84 * @description 认证授权服务器
  85. 85 */
  86. 86 @Configuration
  87. 87 @EnableAuthorizationServer
  88. 88 protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
  89. 89
  90. 90 @Autowired
  91. 91 private AuthenticationManager authenticationManager;
  92. 92
  93. 93 @Autowired
  94. 94 private RedisConnectionFactory connectionFactory;
  95. 95
  96. 96 @Override
  97. 97 public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  98. 98 String finalSecret = "{bcrypt}" + new BCryptPasswordEncoder().encode(CLIENT_SECRET);
  99. 99 //配置客户端,使用密码模式验证鉴权
  100. 100 clients.inMemory()
  101. 101 .withClient(CLIENT_ID)
  102. 102 //密码模式及refresh_token模式
  103. 103 .authorizedGrantTypes(GRANT_TYPE[0], GRANT_TYPE[1])
  104. 104 .scopes("all")
  105. 105 .secret(finalSecret);
  106. 106 }
  107. 107
  108. 108 @Bean
  109. 109 public RedisTokenStore redisTokenStore() {
  110. 110 return new RedisTokenStore(connectionFactory);
  111. 111 }
  112. 112
  113. 113 /**
  114. 114 * @description token及用户信息存储到redis,当然你也可以存储在当前的服务内存,不推荐
  115. 115 * @param endpoints
  116. 116 */
  117. 117 @Override
  118. 118 public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
  119. 119 //token信息存到服务内存
  120. 120 /*endpoints.tokenStore(new InMemoryTokenStore())
  121. 121 .authenticationManager(authenticationManager);*/
  122. 122
  123. 123 //token信息存到redis
  124. 124 endpoints.tokenStore(redisTokenStore()).authenticationManager(authenticationManager);
  125. 125 //配置TokenService参数
  126. 126 DefaultTokenServices tokenService = new DefaultTokenServices();
  127. 127 tokenService.setTokenStore(endpoints.getTokenStore());
  128. 128 tokenService.setSupportRefreshToken(true);
  129. 129 tokenService.setClientDetailsService(endpoints.getClientDetailsService());
  130. 130 tokenService.setTokenEnhancer(endpoints.getTokenEnhancer());
  131. 131 //1小时
  132. 132 tokenService.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1));
  133. 133 //1小时
  134. 134 tokenService.setRefreshTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1));
  135. 135 tokenService.setReuseRefreshToken(false);
  136. 136 endpoints.tokenServices(tokenService);
  137. 137 }
  138. 138
  139. 139 @Override
  140. 140 public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
  141. 141 //允许表单认证
  142. 142 oauthServer.allowFormAuthenticationForClients().tokenKeyAccess("isAuthenticated()")
  143. 143 .checkTokenAccess("permitAll()");
  144. 144 }
  145. 145 }
  146. 146 }

 

  这里有个点要强调一下,就是上面的CustomAuthExceptionHandler ,这是一个自定义返回异常处理。要知道oauth2在登录时用户密码不正确或者权限不足时,oauth2内部携带的Endpoint处理,会默认返回401并且携带的message是它内部默认的英文,例如像这种:

 

 

感觉就很不友好,所以我这里自己去处理AuthException并返回自己想要的数据及数据格式给客户端。 

 

CustomAuthExceptionHandler代码如下:

  1. 1 package com.unionman.humancar.handler;
  2. 2
  3. 3 import com.alibaba.fastjson.JSON;
  4. 4 import com.unionman.humancar.enums.ResponseEnum;
  5. 5 import com.unionman.humancar.vo.ResponseVO;
  6. 6 import lombok.extern.slf4j.Slf4j;
  7. 7 import org.springframework.security.access.AccessDeniedException;
  8. 8 import org.springframework.security.core.AuthenticationException;
  9. 9 import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
  10. 10 import org.springframework.security.web.AuthenticationEntryPoint;
  11. 11 import org.springframework.security.web.access.AccessDeniedHandler;
  12. 12 import org.springframework.stereotype.Component;
  13. 13
  14. 14 import javax.servlet.ServletException;
  15. 15 import javax.servlet.http.HttpServletRequest;
  16. 16 import javax.servlet.http.HttpServletResponse;
  17. 17 import java.io.IOException;
  18. 18
  19. 19 /**
  20. 20 * @author Zhifeng.Zeng
  21. 21 * @description 自定义未授权 token无效 权限不足返回信息处理类
  22. 22 * @date 2019/3/4 15:49
  23. 23 */
  24. 24 @Component
  25. 25 @Slf4j
  26. 26 public class CustomAuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
  27. 27 @Override
  28. 28 public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
  29. 29
  30. 30 Throwable cause = authException.getCause();
  31. 31 response.setContentType("application/json;charset=UTF-8");
  32. 32 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
  33. 33 // CORS "pre-flight" request
  34. 34 response.addHeader("Access-Control-Allow-Origin", "*");
  35. 35 response.addHeader("Cache-Control","no-cache");
  36. 36 response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
  37. 37 response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
  38. 38 response.addHeader("Access-Control-Max-Age", "1800");
  39. 39 if (cause instanceof InvalidTokenException) {
  40. 40 log.error("InvalidTokenException : {}",cause.getMessage());
  41. 41 //Token无效
  42. 42 response.getWriter().write(JSON.toJSONString(ResponseVO.error(ResponseEnum.ACCESS_TOKEN_INVALID)));
  43. 43 } else {
  44. 44 log.error("AuthenticationException : NoAuthentication");
  45. 45 //资源未授权
  46. 46 response.getWriter().write(JSON.toJSONString(ResponseVO.error(ResponseEnum.UNAUTHORIZED)));
  47. 47 }
  48. 48
  49. 49 }
  50. 50
  51. 51 @Override
  52. 52 public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
  53. 53 response.setContentType("application/json;charset=UTF-8");
  54. 54 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
  55. 55 response.addHeader("Access-Control-Allow-Origin", "*");
  56. 56 response.addHeader("Cache-Control","no-cache");
  57. 57 response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
  58. 58 response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
  59. 59 response.addHeader("Access-Control-Max-Age", "1800");
  60. 60 //访问资源的用户权限不足
  61. 61 log.error("AccessDeniedException : {}",accessDeniedException.getMessage());
  62. 62 response.getWriter().write(JSON.toJSONString(ResponseVO.error(ResponseEnum.INSUFFICIENT_PERMISSIONS)));
  63. 63 }
  64. 64 }

 

Spring Security

  这里security主要承担的角色是,用户资源管理,简单地说就是,在客户端发送登录请求的时候,security会将先去根据用户输入的用户名和密码,去查数据库,如果匹配,那么就把相应的用户信息进行一层转换,然后交给认证授权管理器,然后认证授权管理器会根据相应的用户,给他分发一个token(令牌),然后下次进行请求的时候,携带着该token(令牌),认证授权管理器就能根据该token(令牌)去找到相应的用户了。

SecurityConfig代码如下:

  1. 1 package com.unionman.springbootsecurityauth2.config;
  2. 2
  3. 3 import com.unionman.springbootsecurityauth2.domain.CustomUserDetail;
  4. 4 import com.unionman.springbootsecurityauth2.entity.User;
  5. 5 import com.unionman.springbootsecurityauth2.repository.UserRepository;
  6. 6 import lombok.extern.slf4j.Slf4j;
  7. 7 import org.springframework.beans.factory.annotation.Autowired;
  8. 8 import org.springframework.context.annotation.Bean;
  9. 9 import org.springframework.context.annotation.Configuration;
  10. 10 import org.springframework.security.authentication.AuthenticationManager;
  11. 11 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  12. 12 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  13. 13 import org.springframework.security.core.GrantedAuthority;
  14. 14 import org.springframework.security.core.authority.AuthorityUtils;
  15. 15 import org.springframework.security.core.userdetails.UserDetails;
  16. 16 import org.springframework.security.core.userdetails.UserDetailsService;
  17. 17 import org.springframework.security.core.userdetails.UsernameNotFoundException;
  18. 18 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  19. 19 import org.springframework.security.crypto.factory.PasswordEncoderFactories;
  20. 20 import org.springframework.security.crypto.password.PasswordEncoder;
  21. 21 import org.springframework.web.client.RestTemplate;
  22. 22
  23. 23 import java.util.List;
  24. 24
  25. 25 /**
  26. 26 * @description Security核心配置
  27. 27 * @author Zhifeng.Zeng
  28. 28 */
  29. 29 @Configuration
  30. 30 @EnableWebSecurity
  31. 31 @Slf4j
  32. 32 public class SecurityConfig extends WebSecurityConfigurerAdapter {
  33. 33
  34. 34
  35. 35 @Autowired
  36. 36 private UserRepository userRepository;
  37. 37
  38. 38 @Bean
  39. 39 @Override
  40. 40 public AuthenticationManager authenticationManagerBean() throws Exception {
  41. 41 return super.authenticationManagerBean();
  42. 42 }
  43. 43
  44. 44 @Bean
  45. 45 public RestTemplate restTemplate(){
  46. 46 return new RestTemplate();
  47. 47 }
  48. 48
  49. 49 @Bean
  50. 50 @Override
  51. 51 protected UserDetailsService userDetailsService() {
  52. 52 BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
  53. 53 return new UserDetailsService(){
  54. 54 @Override
  55. 55 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  56. 56 log.info("username:{}",username);
  57. 57 User user = userRepository.findUserByAccount(username);
  58. 58 if(user != null){
  59. 59 CustomUserDetail customUserDetail = new CustomUserDetail();
  60. 60 customUserDetail.setUsername(user.getAccount());
  61. 61 customUserDetail.setPassword("{bcrypt}"+bCryptPasswordEncoder.encode(user.getPassword()));
  62. 62 List<GrantedAuthority> list = AuthorityUtils.createAuthorityList(user.getRole().getRole());
  63. 63 customUserDetail.setAuthorities(list);
  64. 64 return customUserDetail;
  65. 65 }else {//返回空
  66. 66 return null;
  67. 67 }
  68. 68
  69. 69 }
  70. 70 };
  71. 71 }
  72. 72
  73. 73 @Bean
  74. 74 PasswordEncoder passwordEncoder() {
  75. 75 return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  76. 76 }
  77. 77 }

 

业务逻辑

  这里我只简单地实现了用户的增删改查以及用户登录的业务逻辑。并没有做太深的业务处理,主要是重点看一下登录的业务逻辑。里面引了几个组件,简单说一下,RestTemplate(http客户端)用于发送http请求,ServerConfig(服务配置)用于获取本服务的ip和端口,RedisUtil(redis工具类) 用户对redis进行缓存的增删改查操作。

 

UserServiceImpl代码如下:

  1. package com.unionman.springbootsecurityauth2.service.impl;
  2.  
  3. import com.unionman.springbootsecurityauth2.config.ServerConfig;
  4. import com.unionman.springbootsecurityauth2.domain.Token;
  5. import com.unionman.springbootsecurityauth2.dto.LoginUserDTO;
  6. import com.unionman.springbootsecurityauth2.dto.UserDTO;
  7. import com.unionman.springbootsecurityauth2.entity.Role;
  8. import com.unionman.springbootsecurityauth2.entity.User;
  9. import com.unionman.springbootsecurityauth2.enums.ResponseEnum;
  10. import com.unionman.springbootsecurityauth2.enums.UrlEnum;
  11. import com.unionman.springbootsecurityauth2.repository.UserRepository;
  12. import com.unionman.springbootsecurityauth2.service.RoleService;
  13. import com.unionman.springbootsecurityauth2.service.UserService;
  14. import com.unionman.springbootsecurityauth2.utils.BeanUtils;
  15. import com.unionman.springbootsecurityauth2.utils.RedisUtil;
  16. import com.unionman.springbootsecurityauth2.vo.LoginUserVO;
  17. import com.unionman.springbootsecurityauth2.vo.ResponseVO;
  18. import com.unionman.springbootsecurityauth2.vo.RoleVO;
  19. import com.unionman.springbootsecurityauth2.vo.UserVO;
  20. import org.springframework.beans.factory.annotation.Autowired;
  21. import org.springframework.stereotype.Service;
  22. import org.springframework.transaction.annotation.Transactional;
  23. import org.springframework.util.LinkedMultiValueMap;
  24. import org.springframework.util.MultiValueMap;
  25. import org.springframework.web.client.RestClientException;
  26. import org.springframework.web.client.RestTemplate;
  27.  
  28. import java.util.ArrayList;
  29. import java.util.List;
  30. import java.util.concurrent.TimeUnit;
  31.  
  32. import static com.unionman.springbootsecurityauth2.config.OAuth2Config.CLIENT_ID;
  33. import static com.unionman.springbootsecurityauth2.config.OAuth2Config.CLIENT_SECRET;
  34. import static com.unionman.springbootsecurityauth2.config.OAuth2Config.GRANT_TYPE;
  35.  
  36. @Service
  37. public class UserServiceImpl implements UserService {
  38.  
  39. @Autowired
  40. private UserRepository userRepository;
  41.  
  42. @Autowired
  43. private RoleService roleService;
  44.  
  45. @Autowired
  46. private RestTemplate restTemplate;
  47.  
  48. @Autowired
  49. private ServerConfig serverConfig;
  50.  
  51. @Autowired
  52. private RedisUtil redisUtil;
  53.  
  54. @Override
  55. @Transactional(rollbackFor = Exception.class)
  56. public void addUser(UserDTO userDTO) {
  57. User userPO = new User();
  58. User userByAccount = userRepository.findUserByAccount(userDTO.getAccount());
  59. if(userByAccount != null){
  60. //此处应该用自定义异常去返回,在这里我就不去具体实现了
  61. try {
  62. throw new Exception("This user already exists!");
  63. } catch (Exception e) {
  64. e.printStackTrace();
  65. }
  66. }
  67. userPO.setCreatedTime(System.currentTimeMillis());
  68. //添加用户角色信息
  69. Role rolePO = roleService.findById(userDTO.getRoleId());
  70. userPO.setRole(rolePO);
  71. BeanUtils.copyPropertiesIgnoreNull(userDTO,userPO);
  72. userRepository.save(userPO);
  73. }
  74.  
  75. @Override
  76. @Transactional(rollbackFor = Exception.class)
  77. public void deleteUser(Integer id) {
  78. User userPO = userRepository.findById(id).get();
  79. if(userPO == null){
  80. //此处应该用自定义异常去返回,在这里我就不去具体实现了
  81. try {
  82. throw new Exception("This user not exists!");
  83. } catch (Exception e) {
  84. e.printStackTrace();
  85. }
  86. }
  87. userRepository.delete(userPO);
  88. }
  89.  
  90. @Override
  91. @Transactional(rollbackFor = Exception.class)
  92. public void updateUser(UserDTO userDTO) {
  93. User userPO = userRepository.findById(userDTO.getId()).get();
  94. if(userPO == null){
  95. //此处应该用自定义异常去返回,在这里我就不去具体实现了
  96. try {
  97. throw new Exception("This user not exists!");
  98. } catch (Exception e) {
  99. e.printStackTrace();
  100. }
  101. }
  102. BeanUtils.copyPropertiesIgnoreNull(userDTO, userPO);
  103. //修改用户角色信息
  104. Role rolePO = roleService.findById(userDTO.getRoleId());
  105. userPO.setRole(rolePO);
  106. userRepository.saveAndFlush(userPO);
  107. }
  108.  
  109. @Override
  110. public ResponseVO<List<UserVO>> findAllUserVO() {
  111. List<User> userPOList = userRepository.findAll();
  112. List<UserVO> userVOList = new ArrayList<>();
  113. userPOList.forEach(userPO->{
  114. UserVO userVO = new UserVO();
  115. BeanUtils.copyPropertiesIgnoreNull(userPO,userVO);
  116. RoleVO roleVO = new RoleVO();
  117. BeanUtils.copyPropertiesIgnoreNull(userPO.getRole(),roleVO);
  118. userVO.setRole(roleVO);
  119. userVOList.add(userVO);
  120. });
  121. return ResponseVO.success(userVOList);
  122. }
  123.  
  124. @Override
  125. public ResponseVO login(LoginUserDTO loginUserDTO) {
  126. MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
  127. paramMap.add("client_id", CLIENT_ID);
  128. paramMap.add("client_secret", CLIENT_SECRET);
  129. paramMap.add("username", loginUserDTO.getAccount());
  130. paramMap.add("password", loginUserDTO.getPassword());
  131. paramMap.add("grant_type", GRANT_TYPE[0]);
  132. Token token = null;
  133. try {
  134. //因为oauth2本身自带的登录接口是"/oauth/token",并且返回的数据类型不能按我们想要的去返回
  135. //但是我的业务需求是,登录接口是"user/login",由于我没研究过要怎么去修改oauth2内部的endpoint配置
  136. //所以这里我用restTemplate(HTTP客户端)进行一次转发到oauth2内部的登录接口,比较简单粗暴
  137. token = restTemplate.postForObject(serverConfig.getUrl() + UrlEnum.LOGIN_URL.getUrl(), paramMap, Token.class);
  138. LoginUserVO loginUserVO = redisUtil.get(token.getValue(), LoginUserVO.class);
  139. if(loginUserVO != null){
  140. //登录的时候,判断该用户是否已经登录过了
  141. //如果redis里面已经存在该用户已经登录过了的信息
  142. //我这边要刷新一遍token信息,不然,它会返回上一次还未过时的token信息给你
  143. //不便于做单点维护
  144. token = oauthRefreshToken(loginUserVO.getRefreshToken());
  145. redisUtil.deleteCache(loginUserVO.getAccessToken());
  146. }
  147. } catch (RestClientException e) {
  148. try {
  149. e.printStackTrace();
  150. //此处应该用自定义异常去返回,在这里我就不去具体实现了
  151. //throw new Exception("username or password error");
  152. } catch (Exception e1) {
  153. e1.printStackTrace();
  154. }
  155. }
  156. //这里我拿到了登录成功后返回的token信息之后,我再进行一层封装,最后返回给前端的其实是LoginUserVO
  157. LoginUserVO loginUserVO = new LoginUserVO();
  158. User userPO = userRepository.findUserByAccount(loginUserDTO.getAccount());
  159. BeanUtils.copyPropertiesIgnoreNull(userPO, loginUserVO);
  160. loginUserVO.setPassword(userPO.getPassword());
  161. loginUserVO.setAccessToken(token.getValue());
  162. loginUserVO.setAccessTokenExpiresIn(token.getExpiresIn());
  163. loginUserVO.setAccessTokenExpiration(token.getExpiration());
  164. loginUserVO.setExpired(token.isExpired());
  165. loginUserVO.setScope(token.getScope());
  166. loginUserVO.setTokenType(token.getTokenType());
  167. loginUserVO.setRefreshToken(token.getRefreshToken().getValue());
  168. loginUserVO.setRefreshTokenExpiration(token.getRefreshToken().getExpiration());
  169. //存储登录的用户
  170. redisUtil.set(loginUserVO.getAccessToken(),loginUserVO,TimeUnit.HOURS.toSeconds(1));
  171. return ResponseVO.success(loginUserVO);
  172. }
  173.  
  174. /**
  175. * @description oauth2客户端刷新token
  176. * @param refreshToken
  177. * @date 2019/03/05 14:27:22
  178. * @author Zhifeng.Zeng
  179. * @return
  180. */
  181. private Token oauthRefreshToken(String refreshToken) {
  182. MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
  183. paramMap.add("client_id", CLIENT_ID);
  184. paramMap.add("client_secret", CLIENT_SECRET);
  185. paramMap.add("refresh_token", refreshToken);
  186. paramMap.add("grant_type", GRANT_TYPE[1]);
  187. Token token = null;
  188. try {
  189. token = restTemplate.postForObject(serverConfig.getUrl() + UrlEnum.LOGIN_URL.getUrl(), paramMap, Token.class);
  190. } catch (RestClientException e) {
  191. try {
  192. //此处应该用自定义异常去返回,在这里我就不去具体实现了
  193. throw new Exception(ResponseEnum.REFRESH_TOKEN_INVALID.getMessage());
  194. } catch (Exception e1) {
  195. e1.printStackTrace();
  196. }
  197. }
  198. return token;
  199. }
  200.  
  201.  
  202. }

 

示例

  这里我使用postman(接口测试工具)去对接口做一些简单的测试。

(1)这里我去发送一个获取用户列表的请求:

 

结果可以看到,由于没有携带token信息,所以返回了如下信息。

 

(2)接下来,我们先去登录。

 

登录成功后,这里会返回一系列信息,记住这个token信息,待会我们尝试使用这个token信息再次请求上面那个获取用户列表接口。

 

(3)携带token去获取用户列表

 

 

可以看到,可以成功拿到接口返回的资源(用户的列表信息)啦。

 

(4)这里测试一下,用户注销的接口。用户注销,会把redis里的token信息全部清除。

 

 

可以看到,注销成功了。那么我们再用这个已经被注销的token再去请求一遍那个获取用户列表接口。

 

很显然,此时已经报token无效了。

 

  接下来,我们对角色的资源分配管理进行一个测试。可以看到我们库里面,项目初始化的时候,就已经创建了一个管理员,我们上面配置已经规定,管理员是拥有所有接口的访问权限的,而普通用户却只有查询权限。我们现在就来测试一下这个效果。

(1)首先我使用该管理员去添加一个普通用户。

 

可以看到,我们返回了添加成功信息了,那么我去查看一下用户列表。

很显然,现在这个用户已经成功添加进去了。

 

(2)接下来,我们用新添加的用户去登录一下该系统。

 

该用户也登录成功了,我们先保存这个token。

 

(3)我们现在携带着刚才登录的普通用户"小王"的token去添加一个普通用户。

         

可以看到,由于"小王"是普通用户,所以是不具备添加用户的权限的。

 

(4)那么我们现在用"小王"这个用户去查询一下用户列表。

 

 

 

可以看到,"小王"这个普通用户是拥有查询用户列表接口的权限的。

总结

  基于Springboot集成security、oauth2实现认证鉴权、资源管理的博文就到这了。描述得其实已经较为详细了,具体代码的示例也给了相关的注释。基本上都是以最简单最基本的方式去做的一个整合Demo。一般实际应用场景里,业务会比较复杂,其中还会有,修改密码,重置密码,主动延时token时长,加密解密等等。这些就根据自己的业务需求去做相应的处理了,基本上的操作都是针对redis去做,因为token相关信息都是存储在redis的。

  具体源码我已经上传到github:https://github.com/githubzengzhifeng/springboot-security-oauth2

原文链接:http://www.cnblogs.com/xiaofengxzzf/p/10733955.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号