springboot +security+oauth2.0 簡(jiǎn)單教程
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
? 作者?|??程序飼養(yǎng)員的自我修養(yǎng)
來源 |? urlify.cn/Jf63En
66套java從入門到精通實(shí)戰(zhàn)課程分享
1、Oauth2簡(jiǎn)介
OAuth(開放授權(quán))是一個(gè)開放標(biāo)準(zhǔn),允許用戶授權(quán)第三方移動(dòng)應(yīng)用訪問他們存儲(chǔ)在另外的服務(wù)提供者上的信息,而不需要將用戶名和密碼提供給第三方移動(dòng)應(yīng)用或分享他們數(shù)據(jù)的所有內(nèi)容,OAuth2.0是OAuth協(xié)議的延續(xù)版本,但不向后兼容OAuth 1.0即完全廢止了OAuth1.0。
2、Oauth2服務(wù)器
授權(quán)服務(wù)器 Authorization Service.
資源服務(wù)器 Resource Service.
?授權(quán)服務(wù)器
授權(quán)服務(wù)器,即服務(wù)提供商專門用來處理認(rèn)證的服務(wù)器。在這里簡(jiǎn)單說一下,主要的功能;
1、通過請(qǐng)求獲得令牌(Token),默認(rèn)的URL是/oauth/token.
? 2、根據(jù)令牌(Token)獲取相應(yīng)的權(quán)限.
資源服務(wù)器
資源服務(wù)器托管了受保護(hù)的用戶賬號(hào)信息,并且對(duì)接口資源進(jìn)行用戶權(quán)限分配及管理,簡(jiǎn)單的說,就是某個(gè)接口(/user/add),我限制只能持有管理員權(quán)限的用戶才能訪問,那么普通用戶就沒有訪問的權(quán)限。
?
以下摘自百度百科圖:

?
3、Demo實(shí)戰(zhàn)加代碼詳解
? ? ??前面我是簡(jiǎn)單地介紹了一下oauth2的一些基本概念,關(guān)于oauth2的深入介紹,可以去搜索更多其它相關(guān)oauth2的博文,在這里推薦一篇前輩的博文https://www.cnblogs.com/Wddpct/p/8976480.html,里面有詳細(xì)的oauth2介紹,包括原理、實(shí)現(xiàn)流程等都講得比較詳細(xì)。我的課題,是主要是以實(shí)戰(zhàn)為主,理論的東西我不想介紹太多, 這里是我個(gè)人去根據(jù)自己的業(yè)務(wù)需求去改造的,存在很多可優(yōu)化的點(diǎn),希望大家可以指出和給予我一些寶貴意見。
接下來開始介紹我的代碼流程吧!
準(zhǔn)備
?新建一個(gè)springboot項(xiàng)目,引入以下依賴。
????????
????????????org.springframework.boot
????????????spring-boot-starter
????????
????????
????????????org.springframework.boot
????????????spring-boot-starter-test
????????????test
????????
????????
????????
????????????org.springframework.boot
????????????spring-boot-starter-web
????????
????????
???????
????????????org.springframework.boot
????????????spring-boot-starter-data-redis
????????
????????
????????
????????????org.projectlombok
????????????lombok
????????
????????
????????
????????????org.springframework.boot
????????????spring-boot-starter-security
????????
????????
????????
????????????org.springframework.security.oauth
????????????spring-security-oauth2
????????????2.3.3.RELEASE
????????
????????
????????
????????????mysql
????????????mysql-connector-java
????????????5.1.47
????????????runtime
????????
????????
????????????org.springframework.boot
????????????spring-boot-starter-data-jpa
????????
????????
????????
????????????com.alibaba
????????????fastjson
????????????1.2.47
????????
????
項(xiàng)目目錄結(jié)構(gòu)
?
?
?
接口
這里我只編寫了一個(gè)AuthController,里面基本所有關(guān)于用戶管理及登錄、注銷的接口我都定義出來了。
AuthController代碼如下:
package?com.unionman.springbootsecurityauth2.controller;
import?com.unionman.springbootsecurityauth2.dto.LoginUserDTO;
import?com.unionman.springbootsecurityauth2.dto.UserDTO;
import?com.unionman.springbootsecurityauth2.service.RoleService;
import?com.unionman.springbootsecurityauth2.service.UserService;
import?com.unionman.springbootsecurityauth2.utils.AssertUtils;
import?com.unionman.springbootsecurityauth2.vo.ResponseVO;
import?lombok.extern.slf4j.Slf4j;
import?org.springframework.beans.factory.annotation.Autowired;
import?org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import?org.springframework.validation.annotation.Validated;
import?org.springframework.web.bind.annotation.*;
import?javax.validation.Valid;
/**
?*?@description?用戶權(quán)限管理
?*?@author?Zhifeng.Zeng
?*?@date?2019/4/19?13:58
?*/
@Slf4j
@Validated
@RestController
@RequestMapping("/auth/")
public?class?AuthController?{
????@Autowired
????private?UserService?userService;
????@Autowired
????private?RoleService?roleService;
????@Autowired
????private?RedisTokenStore?redisTokenStore;
????/**
?????*?@description?添加用戶
?????*?@param?userDTO
?????*?@return
?????*/
????@PostMapping("user")
????public?ResponseVO?add(@Valid?@RequestBody?UserDTO?userDTO){
????????userService.addUser(userDTO);
????????return?ResponseVO.success();
????}
????/**
?????*?@description?刪除用戶
?????*?@param?id
?????*?@return
?????*/
????@DeleteMapping("user/{id}")
????public?ResponseVO?deleteUser(@PathVariable("id")Integer?id){
????????userService.deleteUser(id);
????????return?ResponseVO.success();
????}
????/**
?????*?@descripiton?修改用戶
?????*?@param?userDTO
?????*?@return
?????*/
????@PutMapping("user")
????public?ResponseVO?updateUser(@Valid?@RequestBody?UserDTO?userDTO){
????????userService.updateUser(userDTO);
????????return?ResponseVO.success();
????}
????/**
?????*?@description?獲取用戶列表
?????*?@return
?????*/
????@GetMapping("user")
????public?ResponseVO?findAllUser(){
????????return?userService.findAllUserVO();
????}
????/**
?????*?@description?用戶登錄
?????*?@param?loginUserDTO
?????*?@return
?????*/
????@PostMapping("user/login")
????public?ResponseVO?login(LoginUserDTO?loginUserDTO){
????????return?userService.login(loginUserDTO);
????}
????/**
?????*?@description?用戶注銷
?????*?@param?authorization
?????*?@return
?????*/
????@GetMapping("user/logout")
????public?ResponseVO?logout(@RequestHeader("Authorization")?String?authorization){
????????redisTokenStore.removeAccessToken(AssertUtils.extracteToken(authorization));
????????return?ResponseVO.success();
????}
????/**
?????*?@description?用戶刷新Token
?????*?@param?refreshToken
?????*?@return
?????*/
????@GetMapping("user/refresh/{refreshToken}")
????public?ResponseVO?refresh(@PathVariable(value?=?"refreshToken")?String?refreshToken){
????????return?userService.refreshToken(refreshToken);
????}
????/**
?????*?@description?獲取所有角色列表
?????*?@return
?????*/
????@GetMapping("role")
????public?ResponseVO?findAllRole(){
????????return?roleService.findAllRoleVO();
????}
}
這里所有的接口功能,我都已經(jīng)在業(yè)務(wù)代碼里實(shí)現(xiàn)了,后面相關(guān)登錄、注銷、及刷新token的等接口的業(yè)務(wù)實(shí)現(xiàn)的內(nèi)容我會(huì)貼出來。接下來我需要講解的是關(guān)于oath2及security的詳細(xì)配置。
注意一點(diǎn):這里沒有角色的增刪改功能,只有獲取角色列表功能,為了節(jié)省時(shí)間,我這里的角色列表是項(xiàng)目初始化階段,直接生成的固定的兩個(gè)角色,分別是ROLE_USER(普通用戶)、ROLE_ADMIN(管理員);同時(shí)初始化一個(gè)默認(rèn)的管理員。
?
springbootsecurityauth.sql腳本如下:
SET?NAMES?utf8;
SET?FOREIGN_KEY_CHECKS?=?0;
/**
初始化角色信息
?*/
?CREATE?TABLE?IF?NOT?EXISTS?`um_t_role`(
`id`?INT(11)?PRIMARY?KEY?AUTO_INCREMENT?,
?`description`?VARCHAR(255)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NULL?DEFAULT?NULL,
?`created_time`?BIGINT(20)?NOT?NULL,
?`name`?VARCHAR(255)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NOT?NULL,
?`role`?VARCHAR(255)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NOT?NULL
);
INSERT?IGNORE?INTO?`um_t_role`(id,`name`,description,created_time,role)?VALUES(1,'管理員','管理員擁有所有接口操作權(quán)限',UNIX_TIMESTAMP(NOW()),'ADMIN'),(2,'普通用戶','普通擁有查看用戶列表與修改密碼權(quán)限,不具備對(duì)用戶增刪改權(quán)限',UNIX_TIMESTAMP(NOW()),'USER');
/**
初始化一個(gè)默認(rèn)管理員
?*/
?CREATE?TABLE?IF?NOT?EXISTS?`um_t_user`(
`id`?INT(11)?PRIMARY?KEY?AUTO_INCREMENT?,
?`account`?VARCHAR(255)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NULL?DEFAULT?NULL,
?`description`?VARCHAR(255)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NULL?DEFAULT?NULL,
?`password`?VARCHAR(255)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NULL?DEFAULT?NULL,
?`name`?VARCHAR(255)?CHARACTER?SET?utf8?COLLATE?utf8_general_ci?NOT?NULL
);
INSERT?IGNORE?INTO?`um_t_user`(id,account,`password`,`name`,description)?VALUES(1,'admin','123456','小小豐','系統(tǒng)默認(rèn)管理員');
/**
關(guān)聯(lián)表賦值
?*/
CREATE?TABLE?IF?NOT?EXISTS?`um_t_role_user`(
`role_id`?INT(11),
?`user_id`?INT(11)
);
INSERT?IGNORE?INTO?`um_t_role_user`(role_id,user_id)VALUES(1,1);
配置
application.yml文件:
server:
??port:?8080
spring:
??#?mysql?配置
??datasource:
??????url:?jdbc:mysql://localhost:3306/auth_test?useUnicode=true&characterEncoding=UTF-8&useSSL=false
??????username:?root
??????password:?123456
??????schema:?classpath:springbootsecurityauth.sql
??????sql-script-encoding:?utf-8
??????initialization-mode:?always
??????driver-class-name:?com.mysql.jdbc.Driver
??????#?初始化大小,最小,最大
??????initialSize:?1
??????minIdle:?3
??????maxActive:?20
?????#?配置獲取連接等待超時(shí)的時(shí)間
??????maxWait:?60000
??????#?配置間隔多久才進(jìn)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒
??????timeBetweenEvictionRunsMillis:?60000
??????#?配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒
??????minEvictableIdleTimeMillis:?30000
??????validationQuery:?select?'x'
??????testWhileIdle:?true
??????testOnBorrow:?false
??????testOnReturn:?false
??????#?打開PSCache,并且指定每個(gè)連接上PSCache的大小
??????poolPreparedStatements:?true
??????maxPoolPreparedStatementPerConnectionSize:?20
??????#?配置監(jiān)控統(tǒng)計(jì)攔截的filters,去掉后監(jiān)控界面sql無法統(tǒng)計(jì),'wall'用于防火墻
??????filters:?stat,wall,slf4j
??????#?通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
??????connectionProperties:?druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
#redis?配置
??redis:
????open:?true?#?是否開啟redis緩存??true開啟???false關(guān)閉
????database:?1
????host:?localhost
????port:?6379
????timeout:?5000s??#?連接超時(shí)時(shí)長(毫秒)
????jedis:
??????pool:
????????max-active:?8?#連接池最大連接數(shù)(使用負(fù)值表示沒有限制)
????????max-idle:?8??#連接池中的最大空閑連接
????????max-wait:?-1s?#連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)
????????min-idle:?0??#連接池中的最小空閑連接
#?jpa?配置
??jpa:
????database:?mysql
????show-sql:?false
????hibernate:
??????ddl-auto:?update
????properties:
??????hibernate:
????????dialect:?org.hibernate.dialect.MySQL5Dialect
資源服務(wù)器與授權(quán)服務(wù)器
編寫類Oauth2Config,實(shí)現(xiàn)資源服務(wù)器與授權(quán)服務(wù)器,這里的資源服務(wù)器與授權(quán)服務(wù)器以內(nèi)部類的形式實(shí)現(xiàn)。
Oauth2Config代碼如下:
package?com.unionman.springbootsecurityauth2.config;
import?com.unionman.springbootsecurityauth2.handler.CustomAuthExceptionHandler;
import?org.springframework.beans.factory.annotation.Autowired;
import?org.springframework.context.annotation.Bean;
import?org.springframework.context.annotation.Configuration;
import?org.springframework.data.redis.connection.RedisConnectionFactory;
import?org.springframework.http.HttpMethod;
import?org.springframework.security.authentication.AuthenticationManager;
import?org.springframework.security.config.annotation.web.builders.HttpSecurity;
import?org.springframework.security.config.http.SessionCreationPolicy;
import?org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import?org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import?org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import?org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import?org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import?org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import?org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import?org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import?org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import?org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import?org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import?java.util.concurrent.TimeUnit;
/**
?*?@author?Zhifeng.Zeng
?*?@description?OAuth2服務(wù)器配置
?*/
@Configuration
public?class?OAuth2Config?{
????public?static?final?String?ROLE_ADMIN?=?"ADMIN";
????//訪問客戶端密鑰
????public?static?final?String?CLIENT_SECRET?=?"123456";
????//訪問客戶端ID
????public?static?final?String?CLIENT_ID?="client_1";
????//鑒權(quán)模式
????public?static?final?String?GRANT_TYPE[]?=?{"password","refresh_token"};
????/**
?????*?@description?資源服務(wù)器
?????*/
????@Configuration
????@EnableResourceServer
????protected?static?class?ResourceServerConfiguration?extends?ResourceServerConfigurerAdapter?{
????????@Autowired
????????private?CustomAuthExceptionHandler?customAuthExceptionHandler;
????????@Override
????????public?void?configure(ResourceServerSecurityConfigurer?resources)?{
????????????resources.stateless(false)
????????????????????.accessDeniedHandler(customAuthExceptionHandler)
????????????????????.authenticationEntryPoint(customAuthExceptionHandler);
????????}
????????@Override
????????public?void?configure(HttpSecurity?http)?throws?Exception?{
????????????http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
????????????????????.and()
????????????????????//請(qǐng)求權(quán)限配置
????????????????????.authorizeRequests()
????????????????????//下邊的路徑放行,不需要經(jīng)過認(rèn)證
????????????????????.antMatchers("/oauth/*",?"/auth/user/login").permitAll()
????????????????????//OPTIONS請(qǐng)求不需要鑒權(quán)
????????????????????.antMatchers(HttpMethod.OPTIONS,?"/**").permitAll()
????????????????????//用戶的增刪改接口只允許管理員訪問
????????????????????.antMatchers(HttpMethod.POST,?"/auth/user").hasAnyAuthority(ROLE_ADMIN)
????????????????????.antMatchers(HttpMethod.PUT,?"/auth/user").hasAnyAuthority(ROLE_ADMIN)
????????????????????.antMatchers(HttpMethod.DELETE,?"/auth/user").hasAnyAuthority(ROLE_ADMIN)
????????????????????//獲取角色?權(quán)限列表接口只允許系統(tǒng)管理員及高級(jí)用戶訪問
????????????????????.antMatchers(HttpMethod.GET,?"/auth/role").hasAnyAuthority(ROLE_ADMIN)
????????????????????//其余接口沒有角色限制,但需要經(jīng)過認(rèn)證,只要攜帶token就可以放行
????????????????????.anyRequest()
????????????????????.authenticated();
????????}
????}
????/**
?????*?@description?認(rèn)證授權(quán)服務(wù)器
?????*/
????@Configuration
????@EnableAuthorizationServer
????protected?static?class?AuthorizationServerConfiguration?extends?AuthorizationServerConfigurerAdapter?{
????????@Autowired
????????private?AuthenticationManager?authenticationManager;
????????@Autowired
????????private?RedisConnectionFactory?connectionFactory;
????????@Override
????????public?void?configure(ClientDetailsServiceConfigurer?clients)?throws?Exception?{
????????????String?finalSecret?=?"{bcrypt}"?+?new?BCryptPasswordEncoder().encode(CLIENT_SECRET);
????????????//配置客戶端,使用密碼模式驗(yàn)證鑒權(quán)
????????????clients.inMemory()
????????????????????.withClient(CLIENT_ID)
????????????????????//密碼模式及refresh_token模式
????????????????????.authorizedGrantTypes(GRANT_TYPE[0],?GRANT_TYPE[1])
????????????????????.scopes("all")
????????????????????.secret(finalSecret);
????????}
????????@Bean
????????public?RedisTokenStore?redisTokenStore()?{
????????????return?new?RedisTokenStore(connectionFactory);
????????}
????????/**
?????????*?@description?token及用戶信息存儲(chǔ)到redis,當(dāng)然你也可以存儲(chǔ)在當(dāng)前的服務(wù)內(nèi)存,不推薦
?????????*?@param?endpoints
?????????*/
????????@Override
????????public?void?configure(AuthorizationServerEndpointsConfigurer?endpoints)?{
????????????//token信息存到服務(wù)內(nèi)存
????????????/*endpoints.tokenStore(new?InMemoryTokenStore())
????????????????????.authenticationManager(authenticationManager);*/
????????????//token信息存到redis
????????????endpoints.tokenStore(redisTokenStore()).authenticationManager(authenticationManager);
????????????//配置TokenService參數(shù)
????????????DefaultTokenServices?tokenService?=?new?DefaultTokenServices();
????????????tokenService.setTokenStore(endpoints.getTokenStore());
????????????tokenService.setSupportRefreshToken(true);
????????????tokenService.setClientDetailsService(endpoints.getClientDetailsService());
????????????tokenService.setTokenEnhancer(endpoints.getTokenEnhancer());
????????????//1小時(shí)
????????????tokenService.setAccessTokenValiditySeconds((int)?TimeUnit.HOURS.toSeconds(1));
????????????//1小時(shí)
????????????tokenService.setRefreshTokenValiditySeconds((int)?TimeUnit.HOURS.toSeconds(1));
????????????tokenService.setReuseRefreshToken(false);
????????????endpoints.tokenServices(tokenService);
????????}
????????@Override
????????public?void?configure(AuthorizationServerSecurityConfigurer?oauthServer)?{
????????????//允許表單認(rèn)證
????????????oauthServer.allowFormAuthenticationForClients().tokenKeyAccess("isAuthenticated()")
????????????????????.checkTokenAccess("permitAll()");
????????}
????}
}
這里有個(gè)點(diǎn)要強(qiáng)調(diào)一下,就是上面的CustomAuthExceptionHandler ,這是一個(gè)自定義返回異常處理。要知道oauth2在登錄時(shí)用戶密碼不正確或者權(quán)限不足時(shí),oauth2內(nèi)部攜帶的Endpoint處理,會(huì)默認(rèn)返回401并且攜帶的message是它內(nèi)部默認(rèn)的英文,例如像這種:

?
?
感覺就很不友好,所以我這里自己去處理AuthException并返回自己想要的數(shù)據(jù)及數(shù)據(jù)格式給客戶端。?
?
CustomAuthExceptionHandler代碼如下:
package?com.unionman.humancar.handler;
import?com.alibaba.fastjson.JSON;
import?com.unionman.humancar.enums.ResponseEnum;
import?com.unionman.humancar.vo.ResponseVO;
import?lombok.extern.slf4j.Slf4j;
import?org.springframework.security.access.AccessDeniedException;
import?org.springframework.security.core.AuthenticationException;
import?org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import?org.springframework.security.web.AuthenticationEntryPoint;
import?org.springframework.security.web.access.AccessDeniedHandler;
import?org.springframework.stereotype.Component;
import?javax.servlet.ServletException;
import?javax.servlet.http.HttpServletRequest;
import?javax.servlet.http.HttpServletResponse;
import?java.io.IOException;
/**
?*?@author?Zhifeng.Zeng
?*?@description?自定義未授權(quán)?token無效?權(quán)限不足返回信息處理類
?*?@date?2019/3/4?15:49
?*/
@Component
@Slf4j
public?class?CustomAuthExceptionHandler?implements?AuthenticationEntryPoint,?AccessDeniedHandler?{
????@Override
????public?void?commence(HttpServletRequest?request,?HttpServletResponse?response,?AuthenticationException?authException)?throws?IOException,?ServletException?{
????????Throwable?cause?=?authException.getCause();
????????response.setContentType("application/json;charset=UTF-8");
????????response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
????????//?CORS?"pre-flight"?request
????????response.addHeader("Access-Control-Allow-Origin",?"*");
????????response.addHeader("Cache-Control","no-cache");
????????response.addHeader("Access-Control-Allow-Methods",?"GET,?POST,?PUT,?DELETE,?OPTIONS");
????????response.setHeader("Access-Control-Allow-Headers",?"x-requested-with");
????????response.addHeader("Access-Control-Max-Age",?"1800");
????????if?(cause?instanceof?InvalidTokenException)?{
????????????log.error("InvalidTokenException?:?{}",cause.getMessage());
????????????//Token無效
????????????response.getWriter().write(JSON.toJSONString(ResponseVO.error(ResponseEnum.ACCESS_TOKEN_INVALID)));
????????}?else?{
????????????log.error("AuthenticationException?:?NoAuthentication");
????????????//資源未授權(quán)
????????????response.getWriter().write(JSON.toJSONString(ResponseVO.error(ResponseEnum.UNAUTHORIZED)));
????????}
????}
????@Override
????public?void?handle(HttpServletRequest?request,?HttpServletResponse?response,?AccessDeniedException?accessDeniedException)?throws?IOException,?ServletException?{
????????response.setContentType("application/json;charset=UTF-8");
????????response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
????????response.addHeader("Access-Control-Allow-Origin",?"*");
????????response.addHeader("Cache-Control","no-cache");
????????response.addHeader("Access-Control-Allow-Methods",?"GET,?POST,?PUT,?DELETE,?OPTIONS");
????????response.setHeader("Access-Control-Allow-Headers",?"x-requested-with");
????????response.addHeader("Access-Control-Max-Age",?"1800");
????????//訪問資源的用戶權(quán)限不足
????????log.error("AccessDeniedException?:?{}",accessDeniedException.getMessage());
????????response.getWriter().write(JSON.toJSONString(ResponseVO.error(ResponseEnum.INSUFFICIENT_PERMISSIONS)));
????}
}
Spring Security
這里security主要承擔(dān)的角色是,用戶資源管理,簡(jiǎn)單地說就是,在客戶端發(fā)送登錄請(qǐng)求的時(shí)候,security會(huì)將先去根據(jù)用戶輸入的用戶名和密碼,去查數(shù)據(jù)庫,如果匹配,那么就把相應(yīng)的用戶信息進(jìn)行一層轉(zhuǎn)換,然后交給認(rèn)證授權(quán)管理器,然后認(rèn)證授權(quán)管理器會(huì)根據(jù)相應(yīng)的用戶,給他分發(fā)一個(gè)token(令牌),然后下次進(jìn)行請(qǐng)求的時(shí)候,攜帶著該token(令牌),認(rèn)證授權(quán)管理器就能根據(jù)該token(令牌)去找到相應(yīng)的用戶了。
SecurityConfig代碼如下:
package?com.unionman.springbootsecurityauth2.config;
import?com.unionman.springbootsecurityauth2.domain.CustomUserDetail;
import?com.unionman.springbootsecurityauth2.entity.User;
import?com.unionman.springbootsecurityauth2.repository.UserRepository;
import?lombok.extern.slf4j.Slf4j;
import?org.springframework.beans.factory.annotation.Autowired;
import?org.springframework.context.annotation.Bean;
import?org.springframework.context.annotation.Configuration;
import?org.springframework.security.authentication.AuthenticationManager;
import?org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import?org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import?org.springframework.security.core.GrantedAuthority;
import?org.springframework.security.core.authority.AuthorityUtils;
import?org.springframework.security.core.userdetails.UserDetails;
import?org.springframework.security.core.userdetails.UserDetailsService;
import?org.springframework.security.core.userdetails.UsernameNotFoundException;
import?org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import?org.springframework.security.crypto.factory.PasswordEncoderFactories;
import?org.springframework.security.crypto.password.PasswordEncoder;
import?org.springframework.web.client.RestTemplate;
import?java.util.List;
/**
?*?@description?Security核心配置
?*?@author?Zhifeng.Zeng
?*/
@Configuration
@EnableWebSecurity
@Slf4j
public?class?SecurityConfig?extends?WebSecurityConfigurerAdapter?{
????@Autowired
????private?UserRepository?userRepository;
????@Bean
????@Override
????public?AuthenticationManager?authenticationManagerBean()?throws?Exception?{
????????return?super.authenticationManagerBean();
????}
????@Bean
????public?RestTemplate?restTemplate(){
????????return?new?RestTemplate();
????}
????@Bean
????@Override
????protected?UserDetailsService?userDetailsService()?{
????????BCryptPasswordEncoder?bCryptPasswordEncoder?=?new?BCryptPasswordEncoder();
???????return?new?UserDetailsService(){
???????????@Override
???????????public?UserDetails?loadUserByUsername(String?username)?throws?UsernameNotFoundException?{
???????????????log.info("username:{}",username);
???????????????User?user?=?userRepository.findUserByAccount(username);
???????????????if(user?!=?null){
???????????????????CustomUserDetail?customUserDetail?=?new?CustomUserDetail();
???????????????????customUserDetail.setUsername(user.getAccount());
???????????????????customUserDetail.setPassword("{bcrypt}"+bCryptPasswordEncoder.encode(user.getPassword()));
???????????????????List?list?=?AuthorityUtils.createAuthorityList(user.getRole().getRole());
???????????????????customUserDetail.setAuthorities(list);
???????????????????return?customUserDetail;
???????????????}else?{//返回空
???????????????????return?null;
???????????????}
???????????}
???????};
????}
????@Bean
????PasswordEncoder?passwordEncoder()?{
????????return?PasswordEncoderFactories.createDelegatingPasswordEncoder();
????}
}
業(yè)務(wù)邏輯
這里我只簡(jiǎn)單地實(shí)現(xiàn)了用戶的增刪改查以及用戶登錄的業(yè)務(wù)邏輯。并沒有做太深的業(yè)務(wù)處理,主要是重點(diǎn)看一下登錄的業(yè)務(wù)邏輯。里面引了幾個(gè)組件,簡(jiǎn)單說一下,RestTemplate(http客戶端)用于發(fā)送http請(qǐng)求,ServerConfig(服務(wù)配置)用于獲取本服務(wù)的ip和端口,RedisUtil(redis工具類) 用戶對(duì)redis進(jìn)行緩存的增刪改查操作。
?
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){
????????????//此處應(yīng)該用自定義異常去返回,在這里我就不去具體實(shí)現(xiàn)了
????????????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){
????????????//此處應(yīng)該用自定義異常去返回,在這里我就不去具體實(shí)現(xiàn)了
????????????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){
????????????//此處應(yīng)該用自定義異常去返回,在這里我就不去具體實(shí)現(xiàn)了
????????????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>?findAllUserVO()?{
????????List?userPOList?=?userRepository.findAll();
????????List?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?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?{
????????????//因?yàn)閛auth2本身自帶的登錄接口是"/oauth/token",并且返回的數(shù)據(jù)類型不能按我們想要的去返回
????????????//但是我的業(yè)務(wù)需求是,登錄接口是"user/login",由于我沒研究過要怎么去修改oauth2內(nèi)部的endpoint配置
????????????//所以這里我用restTemplate(HTTP客戶端)進(jìn)行一次轉(zhuǎn)發(fā)到oauth2內(nèi)部的登錄接口,比較簡(jiǎn)單粗暴
????????????token?=?restTemplate.postForObject(serverConfig.getUrl()?+?UrlEnum.LOGIN_URL.getUrl(),?paramMap,?Token.class);
????????????LoginUserVO?loginUserVO?=?redisUtil.get(token.getValue(),?LoginUserVO.class);
????????????if(loginUserVO?!=?null){
????????????????//登錄的時(shí)候,判斷該用戶是否已經(jīng)登錄過了
????????????????//如果redis里面已經(jīng)存在該用戶已經(jīng)登錄過了的信息
????????????????//我這邊要刷新一遍token信息,不然,它會(huì)返回上一次還未過時(shí)的token信息給你
????????????????//不便于做單點(diǎn)維護(hù)
????????????????token?=?oauthRefreshToken(loginUserVO.getRefreshToken());
????????????????redisUtil.deleteCache(loginUserVO.getAccessToken());
????????????}
????????}?catch?(RestClientException?e)?{
????????????try?{
????????????????e.printStackTrace();
????????????????//此處應(yīng)該用自定義異常去返回,在這里我就不去具體實(shí)現(xiàn)了
????????????????//throw?new?Exception("username?or?password?error");
????????????}?catch?(Exception?e1)?{
????????????????e1.printStackTrace();
????????????}
????????}
????????//這里我拿到了登錄成功后返回的token信息之后,我再進(jìn)行一層封裝,最后返回給前端的其實(shí)是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());
????????//存儲(chǔ)登錄的用戶
????????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?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?{
????????????????//此處應(yīng)該用自定義異常去返回,在這里我就不去具體實(shí)現(xiàn)了
????????????????throw?new?Exception(ResponseEnum.REFRESH_TOKEN_INVALID.getMessage());
????????????}?catch?(Exception?e1)?{
????????????????e1.printStackTrace();
????????????}
????????}
????????return?token;
????}
?
?
}
?
示例
這里我使用postman(接口測(cè)試工具)去對(duì)接口做一些簡(jiǎn)單的測(cè)試。
(1)這里我去發(fā)送一個(gè)獲取用戶列表的請(qǐng)求:
?
結(jié)果可以看到,由于沒有攜帶token信息,所以返回了如下信息。
?
(2)接下來,我們先去登錄。

?
登錄成功后,這里會(huì)返回一系列信息,記住這個(gè)token信息,待會(huì)我們嘗試使用這個(gè)token信息再次請(qǐng)求上面那個(gè)獲取用戶列表接口。
?
(3)攜帶token去獲取用戶列表

?

?
可以看到,可以成功拿到接口返回的資源(用戶的列表信息)啦。
?
(4)這里測(cè)試一下,用戶注銷的接口。用戶注銷,會(huì)把redis里的token信息全部清除。
?

?
可以看到,注銷成功了。那么我們?cè)儆眠@個(gè)已經(jīng)被注銷的token再去請(qǐng)求一遍那個(gè)獲取用戶列表接口。

?
很顯然,此時(shí)已經(jīng)報(bào)token無效了。
?
接下來,我們對(duì)角色的資源分配管理進(jìn)行一個(gè)測(cè)試。可以看到我們庫里面,項(xiàng)目初始化的時(shí)候,就已經(jīng)創(chuàng)建了一個(gè)管理員,我們上面配置已經(jīng)規(guī)定,管理員是擁有所有接口的訪問權(quán)限的,而普通用戶卻只有查詢權(quán)限。我們現(xiàn)在就來測(cè)試一下這個(gè)效果。
(1)首先我使用該管理員去添加一個(gè)普通用戶。

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

很顯然,現(xiàn)在這個(gè)用戶已經(jīng)成功添加進(jìn)去了。
?
(2)接下來,我們用新添加的用戶去登錄一下該系統(tǒng)。

?
該用戶也登錄成功了,我們先保存這個(gè)token。
?
(3)我們現(xiàn)在攜帶著剛才登錄的普通用戶"小王"的token去添加一個(gè)普通用戶。
? ? ? ? ?
可以看到,由于"小王"是普通用戶,所以是不具備添加用戶的權(quán)限的。
?
(4)那么我們現(xiàn)在用"小王"這個(gè)用戶去查詢一下用戶列表。

?
總結(jié)
基于Springboot集成security、oauth2實(shí)現(xiàn)認(rèn)證鑒權(quán)、資源管理的博文就到這了。描述得其實(shí)已經(jīng)較為詳細(xì)了,具體代碼的示例也給了相關(guān)的注釋?;旧隙际且宰詈?jiǎn)單最基本的方式去做的一個(gè)整合Demo。一般實(shí)際應(yīng)用場(chǎng)景里,業(yè)務(wù)會(huì)比較復(fù)雜,其中還會(huì)有,修改密碼,重置密碼,主動(dòng)延時(shí)token時(shí)長,加密解密等等。這些就根據(jù)自己的業(yè)務(wù)需求去做相應(yīng)的處理了,基本上的操作都是針對(duì)redis去做,因?yàn)閠oken相關(guān)信息都是存儲(chǔ)在redis的。
粉絲福利:108本java從入門到大神精選電子書領(lǐng)取
???
?長按上方鋒哥微信二維碼?2 秒 備注「1234」即可獲取資料
感謝點(diǎn)贊支持下哈?
