基於Springboot集成security、oauth2實現認證鑒權、資源管理


  

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

 

 

 

 

項目目錄結構

 

 

 

接口

這里我只編寫了一個AuthController,里面基本所有關於用戶管理及登錄、注銷的接口我都定義出來了。

AuthController代碼如下:

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

  這里所有的接口功能,我都已經在業務代碼里實現了,后面相關登錄、注銷、及刷新token的等接口的業務實現的內容我會貼出來。接下來我需要講解的是關於oath2及security的詳細配置。

注意一點:這里沒有角色的增刪改功能,只有獲取角色列表功能,為了節省時間,我這里的角色列表是項目初始化階段,直接生成的固定的兩個角色,分別是ROLE_USER(普通用戶)、ROLE_ADMIN(管理員);同時初始化一個默認的管理員。

 

springbootsecurityauth.sql腳本如下:

 1 SET NAMES utf8;
 2 SET FOREIGN_KEY_CHECKS = 0;
 3 /**
 4 初始化角色信息
 5  */
 6  CREATE TABLE IF NOT EXISTS `um_t_role`(
 7 `id` INT(11) PRIMARY KEY AUTO_INCREMENT ,
 8  `description` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
 9  `created_time` BIGINT(20) NOT NULL,
10  `name` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
11  `role` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL
12 );
13 INSERT IGNORE INTO `um_t_role`(id,`name`,description,created_time,role) VALUES(1,'管理員','管理員擁有所有接口操作權限',UNIX_TIMESTAMP(NOW()),'ADMIN'),(2,'普通用戶','普通擁有查看用戶列表與修改密碼權限,不具備對用戶增刪改權限',UNIX_TIMESTAMP(NOW()),'USER');
14 
15 /**
16 初始化一個默認管理員
17  */
18  CREATE TABLE IF NOT EXISTS `um_t_user`(
19 `id` INT(11) PRIMARY KEY AUTO_INCREMENT ,
20  `account` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
21  `description` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
22  `password` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
23  `name` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL
24 );
25 INSERT IGNORE INTO `um_t_user`(id,account,`password`,`name`,description) VALUES(1,'admin','123456','小小豐','系統默認管理員');
26 
27 /**
28 關聯表賦值
29  */
30 CREATE TABLE IF NOT EXISTS `um_t_role_user`(
31 `role_id` INT(11),
32  `user_id` INT(11)
33 );
34 INSERT IGNORE INTO `um_t_role_user`(role_id,user_id)VALUES(1,1);

 

配置

application.yml文件:

 1 server:
 2   port: 8080
 3 spring:
 4   # mysql 配置
 5   datasource:
 6       url: jdbc:mysql://localhost:3306/auth_test?useUnicode=true&characterEncoding=UTF-8&useSSL=false
 7       username: root
 8       password: 123456
 9       schema: classpath:springbootsecurityauth.sql
10       sql-script-encoding: utf-8
11       initialization-mode: always
12       driver-class-name: com.mysql.jdbc.Driver
13       # 初始化大小,最小,最大
14       initialSize: 1
15       minIdle: 3
16       maxActive: 20
17      # 配置獲取連接等待超時的時間
18       maxWait: 60000
19       # 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
20       timeBetweenEvictionRunsMillis: 60000
21       # 配置一個連接在池中最小生存的時間,單位是毫秒
22       minEvictableIdleTimeMillis: 30000
23       validationQuery: select 'x'
24       testWhileIdle: true
25       testOnBorrow: false
26       testOnReturn: false
27       # 打開PSCache,並且指定每個連接上PSCache的大小
28       poolPreparedStatements: true
29       maxPoolPreparedStatementPerConnectionSize: 20
30       # 配置監控統計攔截的filters,去掉后監控界面sql無法統計,'wall'用於防火牆
31       filters: stat,wall,slf4j
32       # 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
33       connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
34 #redis 配置
35   redis:
36     open: true # 是否開啟redis緩存  true開啟   false關閉
37     database: 1
38     host: localhost
39     port: 6379
40     timeout: 5000s  # 連接超時時長(毫秒)
41     jedis:
42       pool:
43         max-active: 8 #連接池最大連接數(使用負值表示沒有限制)
44         max-idle: 8  #連接池中的最大空閑連接
45         max-wait: -1s #連接池最大阻塞等待時間(使用負值表示沒有限制)
46         min-idle: 0  #連接池中的最小空閑連接
47 
48 # jpa 配置
49   jpa:
50     database: mysql
51     show-sql: false
52     hibernate:
53       ddl-auto: update
54     properties:
55       hibernate:
56         dialect: org.hibernate.dialect.MySQL5Dialect

 

資源服務器與授權服務器

編寫類Oauth2Config,實現資源服務器與授權服務器,這里的資源服務器與授權服務器以內部類的形式實現。

Oauth2Config代碼如下:

 

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

 

  這里有個點要強調一下,就是上面的CustomAuthExceptionHandler ,這是一個自定義返回異常處理。要知道oauth2在登錄時用戶密碼不正確或者權限不足時,oauth2內部攜帶的Endpoint處理,會默認返回401並且攜帶的message是它內部默認的英文,例如像這種:

 

 

感覺就很不友好,所以我這里自己去處理AuthException並返回自己想要的數據及數據格式給客戶端。 

 

CustomAuthExceptionHandler代碼如下:

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

 

Spring Security

  這里security主要承擔的角色是,用戶資源管理,簡單地說就是,在客戶端發送登錄請求的時候,security會將先去根據用戶輸入的用戶名和密碼,去查數據庫,如果匹配,那么就把相應的用戶信息進行一層轉換,然后交給認證授權管理器,然后認證授權管理器會根據相應的用戶,給他分發一個token(令牌),然后下次進行請求的時候,攜帶着該token(令牌),認證授權管理器就能根據該token(令牌)去找到相應的用戶了。

SecurityConfig代碼如下:

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

 

業務邏輯

  這里我只簡單地實現了用戶的增刪改查以及用戶登錄的業務邏輯。並沒有做太深的業務處理,主要是重點看一下登錄的業務邏輯。里面引了幾個組件,簡單說一下,RestTemplate(http客戶端)用於發送http請求,ServerConfig(服務配置)用於獲取本服務的ip和端口,RedisUtil(redis工具類) 用戶對redis進行緩存的增刪改查操作。

 

UserServiceImpl代碼如下:

package com.unionman.springbootsecurityauth2.service.impl;

import com.unionman.springbootsecurityauth2.config.ServerConfig;
import com.unionman.springbootsecurityauth2.domain.Token;
import com.unionman.springbootsecurityauth2.dto.LoginUserDTO;
import com.unionman.springbootsecurityauth2.dto.UserDTO;
import com.unionman.springbootsecurityauth2.entity.Role;
import com.unionman.springbootsecurityauth2.entity.User;
import com.unionman.springbootsecurityauth2.enums.ResponseEnum;
import com.unionman.springbootsecurityauth2.enums.UrlEnum;
import com.unionman.springbootsecurityauth2.repository.UserRepository;
import com.unionman.springbootsecurityauth2.service.RoleService;
import com.unionman.springbootsecurityauth2.service.UserService;
import com.unionman.springbootsecurityauth2.utils.BeanUtils;
import com.unionman.springbootsecurityauth2.utils.RedisUtil;
import com.unionman.springbootsecurityauth2.vo.LoginUserVO;
import com.unionman.springbootsecurityauth2.vo.ResponseVO;
import com.unionman.springbootsecurityauth2.vo.RoleVO;
import com.unionman.springbootsecurityauth2.vo.UserVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static com.unionman.springbootsecurityauth2.config.OAuth2Config.CLIENT_ID;
import static com.unionman.springbootsecurityauth2.config.OAuth2Config.CLIENT_SECRET;
import static com.unionman.springbootsecurityauth2.config.OAuth2Config.GRANT_TYPE;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleService roleService;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private ServerConfig serverConfig;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addUser(UserDTO userDTO)  {
        User userPO = new User();
        User userByAccount = userRepository.findUserByAccount(userDTO.getAccount());
        if(userByAccount != null){
            //此處應該用自定義異常去返回,在這里我就不去具體實現了
            try {
                throw new Exception("This user already exists!");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        userPO.setCreatedTime(System.currentTimeMillis());
        //添加用戶角色信息
        Role rolePO = roleService.findById(userDTO.getRoleId());
        userPO.setRole(rolePO);
        BeanUtils.copyPropertiesIgnoreNull(userDTO,userPO);
        userRepository.save(userPO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteUser(Integer id)  {
        User userPO = userRepository.findById(id).get();
        if(userPO == null){
            //此處應該用自定義異常去返回,在這里我就不去具體實現了
            try {
                throw new Exception("This user not exists!");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        userRepository.delete(userPO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateUser(UserDTO userDTO) {
        User userPO = userRepository.findById(userDTO.getId()).get();
        if(userPO == null){
            //此處應該用自定義異常去返回,在這里我就不去具體實現了
            try {
                throw new Exception("This user not exists!");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        BeanUtils.copyPropertiesIgnoreNull(userDTO, userPO);
        //修改用戶角色信息
        Role rolePO = roleService.findById(userDTO.getRoleId());
        userPO.setRole(rolePO);
        userRepository.saveAndFlush(userPO);
    }

    @Override
    public ResponseVO<List<UserVO>> findAllUserVO() {
        List<User> userPOList = userRepository.findAll();
        List<UserVO> userVOList = new ArrayList<>();
        userPOList.forEach(userPO->{
            UserVO userVO = new UserVO();
            BeanUtils.copyPropertiesIgnoreNull(userPO,userVO);
            RoleVO roleVO = new RoleVO();
            BeanUtils.copyPropertiesIgnoreNull(userPO.getRole(),roleVO);
            userVO.setRole(roleVO);
            userVOList.add(userVO);
        });
        return ResponseVO.success(userVOList);
    }

    @Override
    public ResponseVO login(LoginUserDTO loginUserDTO) {
        MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
        paramMap.add("client_id", CLIENT_ID);
        paramMap.add("client_secret", CLIENT_SECRET);
        paramMap.add("username", loginUserDTO.getAccount());
        paramMap.add("password", loginUserDTO.getPassword());
        paramMap.add("grant_type", GRANT_TYPE[0]);
        Token token = null;
        try {
            //因為oauth2本身自帶的登錄接口是"/oauth/token",並且返回的數據類型不能按我們想要的去返回
            //但是我的業務需求是,登錄接口是"user/login",由於我沒研究過要怎么去修改oauth2內部的endpoint配置
            //所以這里我用restTemplate(HTTP客戶端)進行一次轉發到oauth2內部的登錄接口,比較簡單粗暴
            token = restTemplate.postForObject(serverConfig.getUrl() + UrlEnum.LOGIN_URL.getUrl(), paramMap, Token.class);
            LoginUserVO loginUserVO = redisUtil.get(token.getValue(), LoginUserVO.class);
            if(loginUserVO != null){
                //登錄的時候,判斷該用戶是否已經登錄過了
                //如果redis里面已經存在該用戶已經登錄過了的信息
                //我這邊要刷新一遍token信息,不然,它會返回上一次還未過時的token信息給你
                //不便於做單點維護
                token = oauthRefreshToken(loginUserVO.getRefreshToken());
                redisUtil.deleteCache(loginUserVO.getAccessToken());
            }
        } catch (RestClientException e) {
            try {
                e.printStackTrace();
                //此處應該用自定義異常去返回,在這里我就不去具體實現了
                //throw new Exception("username or password error");
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
        //這里我拿到了登錄成功后返回的token信息之后,我再進行一層封裝,最后返回給前端的其實是LoginUserVO
        LoginUserVO loginUserVO = new LoginUserVO();
        User userPO = userRepository.findUserByAccount(loginUserDTO.getAccount());
        BeanUtils.copyPropertiesIgnoreNull(userPO, loginUserVO);
        loginUserVO.setPassword(userPO.getPassword());
        loginUserVO.setAccessToken(token.getValue());
        loginUserVO.setAccessTokenExpiresIn(token.getExpiresIn());
        loginUserVO.setAccessTokenExpiration(token.getExpiration());
        loginUserVO.setExpired(token.isExpired());
        loginUserVO.setScope(token.getScope());
        loginUserVO.setTokenType(token.getTokenType());
        loginUserVO.setRefreshToken(token.getRefreshToken().getValue());
        loginUserVO.setRefreshTokenExpiration(token.getRefreshToken().getExpiration());
        //存儲登錄的用戶
        redisUtil.set(loginUserVO.getAccessToken(),loginUserVO,TimeUnit.HOURS.toSeconds(1));
        return ResponseVO.success(loginUserVO);
    }

    /**
     * @description oauth2客戶端刷新token
     * @param refreshToken
     * @date 2019/03/05 14:27:22
     * @author Zhifeng.Zeng
     * @return
     */
    private Token oauthRefreshToken(String refreshToken) {
        MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
        paramMap.add("client_id", CLIENT_ID);
        paramMap.add("client_secret", CLIENT_SECRET);
        paramMap.add("refresh_token", refreshToken);
        paramMap.add("grant_type", GRANT_TYPE[1]);
        Token token = null;
        try {
            token = restTemplate.postForObject(serverConfig.getUrl() + UrlEnum.LOGIN_URL.getUrl(), paramMap, Token.class);
        } catch (RestClientException e) {
            try {
                //此處應該用自定義異常去返回,在這里我就不去具體實現了
                throw new Exception(ResponseEnum.REFRESH_TOKEN_INVALID.getMessage());
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
        return token;
    }


}

 

示例

  這里我使用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM