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