SpringCould整合spring-security+oauth2(親測)
1.OAuth2 概念
-
OAuth2 實際上是一個關於受權的網絡標準,它制定了設計思路和運行流程,利用這個標準咱們實際上是能夠本身實現 OAuth2 的認證過程的。html
OAuth 2 有四種受權模式:前端
-
受權碼模式(authorization code)java
-
簡化模式(implicit)mysql
-
密碼模式(resource owner password credentials)git
-
客戶端模式(client credentials)github
具體 OAuth2 是什麼,能夠參考這篇文章。(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)web
2.什麼狀況下須要用 OAuth2
例子:redis
首先你們最熟悉的就是幾乎每一個人都用過的,好比用微信登陸、用 QQ 登陸、用微博登陸、用 Google 帳號登陸、用 github 受權登陸等等,這些都是典型的 OAuth2 使用場景。假設咱們作了一個本身的服務平臺,若是不使用 OAuth2 登陸方式,那麼咱們須要用戶先完成註冊,而後用註冊號的帳號密碼或者用手機驗證碼登陸。而使用了 OAuth2 以後,相信不少人使用過、甚至開發過公衆號網頁服務、小程序,當咱們進入網頁、小程序界面,第一次使用就無需註冊,直接使用微信受權登陸便可,大大提升了使用效率。由於每一個人都有微信號,有了微信就能夠立刻使用第三方服務,這體驗不要太好了。而對於咱們的服務來講,咱們也不須要存儲用戶的密碼,只要存儲認證平臺返回的惟一ID 和用戶信息便可。
以上是使用了 OAuth2 的受權碼模式,利用第三方的權威平臺實現用戶身份的認證。固然了,若是你的公司內部有不少個服務,能夠專門提取出一個認證中心,這個認證中心就充當上面所說的權威認證平臺的角色,全部的服務都要到這個認證中心作認證spring
這樣一說,發現沒,這其實就是個單點登陸的功能。這就是另一種使用場景,對於多服務的平臺,可使用 OAuth2 實現服務的單點登陸,只作一次登陸,就能夠在多個服務中自由穿行,固然僅限於受權範圍內的服務和接口。sql
3.具體使用
OAuth2 實際上是一個關於受權的網絡標準,它制定了設計思路和運行流程,利用這個標準咱們實際上是能夠本身實現 OAuth2 的認證過程的。今天要介紹的 spring-cloud-starter-oauth2 ,實際上是 Spring Cloud 按照 OAuth2 的標準並結合 spring-security 封裝好的一個具體實現。
3.1 系統架構說明
- 認證服務:OAuth2 主要實現端,Token 的生成、刷新、驗證都在認證中心完成。
- 後臺服務: 接收到請求後會到認證中心驗證
- 前端:認證服務、後臺服務之間的聯調
上圖描述了使用了 前端與OAuth2 認證服務、微服務間的請求過程。大體的過程就是前端用用戶名和密碼到後臺服務登陸,成功後後臺服務到認證服務端換取 token,返回給前端,前端拿着 token 去各個微服務請求數據接口,通常這個 token 是放到 header 中的。當微服務接到請求後,先要拿着 token 去認證服務端檢查 token 的合法性,若是合法,再根據用戶所屬的角色及具備的權限動態的返回數據
3.2 建立並配置認證服務端
配置最多的就是認證服務端,驗證帳號、密碼,存儲 token,檢查 token ,刷新 token 等都是認證服務端的工做。
3.2.1 引入須要的maven包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
之因此引入 redis 包,是由於下面會介紹一種用 redis 存儲 token 的方式。
3.2.2 配置好 application.yml
spring: application: name: auth-server redis: database: 2 host: localhost port: 6379 server: port: 6001 management: endpoint: health: enabled: true
3.2.3 spring security 基礎配置
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 容許匿名訪問全部接口 主要是 oauth 接口 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/**").permitAll(); } }
使用@EnableWebSecurity
註解修飾,並繼承自WebSecurityConfigurerAdapter
類。
這個類的重點就是聲明 PasswordEncoder
和 AuthenticationManager
兩個 Bean。稍後會用到。其中 BCryptPasswordEncoder
是一個密碼加密工具類,它能夠實現不可逆的加密,AuthenticationManager
是爲了實現 OAuth2 的 password 模式必需要指定的受權管理 Bean。
3.2.4 實現 UserDetailsService
若是你以前用過 Security 的話,那確定對這個類很熟悉,它是實現用戶身份驗證的一種方式,也是最簡單方便的一種。另外還有結合 AuthenticationProvider
的方式,有機會講 Security 的時候再展開來說吧。
UserDetailsService
的核心就是 loadUserByUsername
方法,它要接收一個字符串參數,也就是傳過來的用戶名,返回一個 UserDetails
對象。
@Component(value = "kiteUserDetailsService") public class KiteUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; /** * Security的登陸,User賦予權限 * * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (StringUtils.isBlank(username)) { throw new UsernameNotFoundException("the username is not null"); } //校驗用戶是否存在 User user = userRepository.getById(username); if (null == user){ throw new UsernameNotFoundException("the user is not exist"); } //給用戶添加角色權限 String role = user.getRole(); List<SimpleGrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(role)); //返回用戶token return new org.springframework.security.core.userdetails.User(username, user.getOauthpassword(), authorities); }
3.2.5 OAuth2 配置文件
建立一個配置文件繼承自 AuthorizationServerConfigurerAdapter
@Configuration @EnableAuthorizationServer public class OAuth2Config extends AuthorizationServerConfigurerAdapter { /** * 指定密碼的加密方式 */ @Autowired public PasswordEncoder passwordEncoder; /** * 該對象爲刷新token提供支持 */ @Autowired public UserDetailsService kiteUserDetailsService; /** * 該對象用來支持password模式 */ @Autowired private AuthenticationManager authenticationManager; /** * 該對象用來說令牌信息存儲到內存中 */ @Autowired private TokenStore redisTokenStore; /** * 密碼模式下配置認證管理器 AuthenticationManager,而且設置 AccessToken的存儲介質tokenStore,如 果不設置,則會默認使用內存當作存儲介質。 * 而該AuthenticationManager將會注入 2個Bean對象用以檢查(認證) * 一、ClientDetailsService的實現類 JdbcClientDetailsService (檢查 ClientDetails 對象) * 二、UserDetailsService的實現類 KiteUserDetailsService (檢查 UserDetails 對象) */ @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception { /** redis token 方式*/ endpoints.authenticationManager(authenticationManager) .userDetailsService(kiteUserDetailsService) .tokenStore(redisTokenStore); } /** * 配置 oauth_client_details【client_id和client_secret等】信息的認證【檢查ClientDetails的合 法性】服務 * 設置 認證信息的來源:數據庫 (可選項:數據庫和內存,使用內存通常用來做測試) * 自動注入:ClientDetailsService的實現類 JdbcClientDetailsService (檢查 ClientDetails 對 象) * 1.inMemory 方式存儲的,將配置保存到內存中,至關於硬編碼了。正式環境下的作法是持久化到數據庫中,好比 mysql 中。 * 2. secret加密是client_id:secret 而後經過base64編碼後的字符串 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //添加客戶端信息 //使用內存存儲OAuth客服端信息 clients.inMemory() // client_id 客戶單ID .withClient("order-client") // client_secret 客戶單祕鑰 .secret(passwordEncoder.encode("order-secret-8888")) // 該客戶端容許的受權類型,不一樣的類型,則獲取token的方式不同 .authorizedGrantTypes("refresh_token", "authorization_code", "password") // token 有效期 .accessTokenValiditySeconds(3600) // 容許的受權範圍 .scopes("all") .and() .withClient("user-client") .secret(passwordEncoder.encode("user-secret-8888")) .authorizedGrantTypes("refresh_token", "authorization_code", "password") .accessTokenValiditySeconds(3600) .scopes("all"); } /** * 配置:安全檢查流程 * 默認過濾器:BasicAuthenticationFilter * 一、oauth_client_details表中clientSecret字段加密【ClientDetails屬性secret】 * 二、CheckEndpoint類的接口 oauth/check_token 無需通過過濾器過濾,默認值:denyAll() */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { ///容許客戶表單認證 security.allowFormAuthenticationForClients(); //對於CheckEndpoint控制器[框架自帶的校驗]的/oauth/check端點容許全部客戶端發送器請求而不會被 Spring-security攔截 security.checkTokenAccess("isAuthenticated()"); security.tokenKeyAccess("isAuthenticated()"); } }
有三個 configure 方法的重寫。
AuthorizationServerEndpointsConfigurer
參數的重寫
endpoints.authenticationManager(authenticationManager) .userDetailsService(kiteUserDetailsService) .tokenStore(redisTokenStore);
authenticationManage()
調用此方法才能支持 password 模式。
userDetailsService()
設置用戶驗證服務。
tokenStore()
指定 token 的存儲方式。
redisTokenStore Bean 的定義以下:
@Configuration public class RedisTokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore (){ return new RedisTokenStore(redisConnectionFactory); } }
ClientId、Client-Secret:這兩個參數對應請求端定義的 cleint-id 和 client-secret
authorizedGrantTypes 能夠包括以下幾種設置中的一種或多種:
- authorization_code:受權碼類型。
- implicit:隱式受權類型。
- password:資源全部者(即用戶)密碼類型。
- client_credentials:客戶端憑據(客戶端ID以及Key)類型。
- refresh_token:經過以上受權得到的刷新令牌來獲取新的令牌。
accessTokenValiditySeconds:token 的有效期
scopes:用來限制客戶端訪問的權限,在換取的 token 的時候會帶上 scope 參數,只有在 scopes 定義內的,才能夠正常換取 token。
上面代碼中是使用 inMemory 方式存儲的,將配置保存到內存中,至關於硬編碼了。正式環境下的作法是持久化到數據庫中,好比 mysql 中。(優化認證服務有實例)
3.3.6 建立數據庫SpringCloud、user表、實體User、UserRepository
實體bean
@Entity @Table( name = "user" ) @Setter @Getter public class User implements Serializable { @Id @GeneratedValue(generator = "uuidGenerator") @GenericGenerator(name = "uuidGenerator", strategy = "uuid") @Column(name = "id", nullable = false) private String id; @Column(name = "username") private String username; @Column(name = "oauth_password") private String oauthpassword; @Column(name = "role") private String role; }
jpa接口
public interface UserRepository extends JpaRepository<User, String> { @Query("select r from User r where r.id = ?1 ") User getById(String username); }
user表
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, `oauth_password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, `role` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of user -- ---------------------------- INSERT INTO `user` VALUES ('1', 'admin', '$2a$10$D3PEtxvJ.N9Ko6osFaO4SO/jYcC8v7RHP34gZNk5THMvX7H5g8/NS', 'ROLE_ADMIN'); INSERT INTO `user` VALUES ('2', 'Custon', '$2a$10$D3PEtxvJ.N9Ko6osFaO4SO/jYcC8v7RHP34gZNk5THMvX7H5g8/NS', 'ROLE_ADMIN'); SET FOREIGN_KEY_CHECKS = 1;
3.2.6 啓動認證服務
完成以後,啓動項目,若是你用的是 IDEA 會在下方的 Mapping 窗口中看到 oauth2 相關的 RESTful 接口。
主要有以下幾個:
POST /oauth/authorize 受權碼模式認證受權接口 GET/POST /oauth/token 獲取 token 的接口 POST /oauth/check_token 檢查 token 合法性接口
3.3 建立用戶客戶端項目
上面建立完成了認證服務端,下面開始建立一個客戶端,對應到咱們系統中的業務相關的微服務。咱們假設這個微服務項目是管理用戶相關數據的,因此叫作用戶客戶端。
3.3.1 引用相關的 maven 包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
3.3.2 application.yml 配置文件
spring: application: name: client-user redis: database: 2 host: localhost port: 6379 server: port: 6101 servlet: context-path: /client-user security: oauth2: client: client-id: user-client client-secret: user-secret-8888 user-authorization-uri: http://localhost:6001/oauth/authorize access-token-uri: http://localhost:6001/oauth/token resource: id: user-client user-info-uri: user-info authorization: check-token-access: http://localhost:6001/oauth/check_token
上面是常規配置信息以及 redis 配置,重點是下面的 security 的配置,這裏的配置稍有不注意就會出現 401 或者其餘問題。
client-id、client-secret 要和認證服務中的配置一致,若是是使用 inMemory 仍是 jdbc 方式。
user-authorization-uri 是受權碼認證方式須要的,下一篇文章再說。
access-token-uri 是密碼模式須要用到的獲取 token 的接口。
authorization.check-token-access 也是關鍵信息,當此服務端接收到來自客戶端端的請求後,須要拿着請求中的 token 到認證服務端作 token 驗證,就是請求的這個接口.
3.3.3 資源配置文件
在 OAuth2 的概念裏,全部的接口都被稱爲資源,接口的權限也就是資源的權限,因此 Spring Security OAuth2 中提供了關於資源的註解 @EnableResourceServer
,和 @EnableWebSecurity
的做用相似。
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Value("${security.oauth2.client.client-id}") private String clientId; @Value("${security.oauth2.client.client-secret}") private String secret; @Value("${security.oauth2.authorization.check-token-access}") private String checkTokenEndpointUrl; @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore (){ return new RedisTokenStore(redisConnectionFactory); } @Bean public RemoteTokenServices tokenService() { RemoteTokenServices tokenService = new RemoteTokenServices(); tokenService.setClientId(clientId); tokenService.setClientSecret(secret); tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl); return tokenService; } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenServices(tokenService()); } }
由於使用的是 redis 做爲 token 的存儲,因此須要特殊配置一下叫作 tokenService 的 Bean,經過這個 Bean 才能實現 token 的驗證。
3.3.4 最後,添加一個 RESTful 接口
@Slf4j @RestController public class UserController { @GetMapping(value = "get") @PreAuthorize("hasAnyRole('ROLE_ADMIN')") public Object get(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return authentication.getName(); } }
一個 RESTful 方法,只有當訪問用戶具備 ROLE_ADMIN 權限時才能訪問,不然返回 401 未受權。
經過 Authentication 參數或者 SecurityContextHolder.getContext().getAuthentication()
能夠拿到受權信息進行查看。
3.4 測試
3.4.1 獲取token
http://localhost:6001/oauth/token?username=2&password=123456&grant_type=password&scope=all&client_id=user-client&client_secret=user-secret-8888
3.4.3 校驗token
接口地址 http://localhost:6001/oauth/check_token?token=5f861834-9c6f-4424-af1d-df35fefddee3
正常返回結果:
{ "active": true, "exp": 1597915851, "user_name": "2", "authorities": [ "ROLE_ADMIN" ], "client_id": "user-client", "scope": [ "all" ] }
校驗失敗結果:
{ "error": "invalid_token", "error_description": "Token was not recognised" }
3.4.3 獲取refresh_token
grant_type: refresh_token
refresh_token: 從獲取token裏面取出
3.4.2 客戶端攜帶token訪問接口
http://localhost:6101/client-user/get
返回結果: 「2」 (登陸username)
token到了過時時間,再次訪問,返回結果
{ "error": "invalid_token", "error_description": "f7520be0-fb2c-4386-9ffc-e64977314b2f" }
3.5 優化方案
3.5.1 認證服務OAuth2Config的configure(ClientDetailsServiceConfigurer clients) 換成數據庫存儲
@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //添加客戶端信息 //使用內存存儲OAuth客服端信息 clients.inMemory() // client_id 客戶單ID .withClient("order-client") // client_secret 客戶單祕鑰 .secret(passwordEncoder.encode("order-secret-8888")) // 該客戶端容許的受權類型,不一樣的類型,則獲取token的方式不同 .authorizedGrantTypes("refresh_token", "authorization_code", "password") // token 有效期 .accessTokenValiditySeconds(3600) // 容許的受權範圍 .scopes("all") .and() .withClient("user-client") .secret(passwordEncoder.encode("user-secret-8888")) .authorizedGrantTypes("refresh_token", "authorization_code", "password") .accessTokenValiditySeconds(3600) .scopes("all"); }
把OAuth2Config.java文件的configure(ClientDetailsServiceConfigurer clients)替換成下面的
@Autowired private DataSource dataSource; /** * jdbc配置 * * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource); jcsb.passwordEncoder(passwordEncoder); }
在application.yml添加數據庫鏈接
#數據庫鏈接 datasource: url: jdbc:mysql://localhost:3306/springcloud?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false username: root password: 123456
- 在數據庫中增長表,並插入數據
create table oauth_client_details ( client_id VARCHAR(256) PRIMARY KEY, resource_ids VARCHAR(256), client_secret VARCHAR(256), scope VARCHAR(256), authorized_grant_types VARCHAR(256), web_server_redirect_uri VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additional_information VARCHAR(4096), autoapprove VARCHAR(256) ); INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('user-client', '$2a$10$o2l5kA7z.Caekp72h5kU7uqdTDrlamLq.57M1F6ulJln9tRtOJufq', 'all', 'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true); INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('order-client', '$2a$10$GoIOhjqFKVyrabUNcie8d.ADX.qZSxpYbO6YK4L2gsNzlCIxEUDlW', 'all', 'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
注意: client_secret 字段不能直接是 secret 的原始值,須要通過加密。由於是用的 BCryptPasswordEncoder
,因此最終插入的值應該是通過 BCryptPasswordEncoder.encode()
以後的值。
@RunWith(SpringRunner.class) @SpringBootTest(classes = SecurityServerSystemApplication.class) public class OAuth2PasswordTest { @Autowired public PasswordEncoder passwordEncoder; @Test public void passwordEncode() { //secret System.out.println(passwordEncoder.encode("user-secret-8888")); } }
3.6 JWT替換 redisToke
上面 token 的存儲用的是 redis 的方案,Spring Security OAuth2 還提供了 jdbc 和 jwt 的支持,jdbc 的暫不考慮,如今來介紹用 JWT 的方式來實現 token 的存儲。
用 JWT 的方式就不用把 token 再存儲到服務端了,JWT 有本身特殊的加密方式,能夠有效的防止數據被篡改,只要不把用戶密碼等關鍵信息放到 JWT 裏就能夠保證安全性。
3.6.1 認證服務端改造
3.6.1.1 添加 JwtConfig 配置類
@Configuration public class JwtTokenConfig { @Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("dev"); return accessTokenConverter; } }
JwtAccessTokenConverter
是爲了作 JWT 數據轉換,這樣作是由於 JWT 有自身獨特的數據格式。若是沒有了解過 JWT ,能夠搜索一下先了解一下。
3.6.1.2 更改 OAuthConfig 配置類
@Autowired private TokenStore jwtTokenStore; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception { /** * 普通 jwt 模式 */ endpoints.tokenStore(jwtTokenStore) .accessTokenConverter(jwtAccessTokenConverter) .userDetailsService(kiteUserDetailsService) /** * 支持 password 模式 */ .authenticationManager(authenticationManager); }
注入 JWT 相關的 Bean,而後修改 configure(final AuthorizationServerEndpointsConfigurer endpoints)
方法爲 JWT 存儲模式。
3.6.2 改造用戶客戶端
3.6.2.1 修改 application.yml 配置文件
security: oauth2: client: client-id: user-client client-secret: user-secret-8888 user-authorization-uri: http://localhost:6001/oauth/authorize access-token-uri: http://localhost:6001/oauth/token resource: jwt: key-uri: http://localhost:6001/oauth/token_key key-value: dev
注意認證服務端 JwtAccessTokenConverter
設置的 SigningKey 要和配置文件中的 key-value 相同,否則會致使沒法正常解碼 JWT ,致使驗證不經過。
3.6.2.2 ResourceServerConfig 類的配置
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("dev"); accessTokenConverter.setVerifierKey("dev"); return accessTokenConverter; } @Autowired private TokenStore jwtTokenStore; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenStore(jwtTokenStore); } }
3.6.3 測試
跟上面同樣(這裏就不重複了)
3.6.4 加強 JWT
若是我想在 JWT 中加入額外的字段(比方說用戶的其餘信息)怎麼辦呢,固然能夠。spring security oauth2 提供了 TokenEnhancer
加強器。其實不光 JWT ,RedisToken 的方式一樣能夠。
3.6.4.1 OAuthConfig 配置類修改
聲明一個加強器
public class JWTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { Map<String, Object> info = new HashMap<>(); info.put("jwt-ext", "JWT 擴展信息"); ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info); return oAuth2AccessToken; } }
經過 oAuth2Authentication 能夠拿到用戶名等信息,經過這些咱們能夠在這裏查詢數據庫或者緩存獲取更多的信息,而這些信息均可以做爲 JWT 擴展信息加入其中。
在JwtTokenConfig.java 注入加強器 TokenEnhancer
@Configuration public class JwtTokenConfig { @Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("dev"); return accessTokenConverter; } @Bean public TokenEnhancer jwtTokenEnhancer() { return new JWTokenEnhancer(); } }
OAuthConfig.java 修改 configure(final AuthorizationServerEndpointsConfigurer endpoints)
方法
@Override public void configure( final AuthorizationServerEndpointsConfigurer endpoints ) throws Exception{ /** * jwt 加強模式 */ TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> enhancerList = new ArrayList<>(); enhancerList.add( jwtTokenEnhancer ); enhancerList.add( jwtAccessTokenConverter ); enhancerChain.setTokenEnhancers( enhancerList ); endpoints.tokenStore( jwtTokenStore ) .userDetailsService( kiteUserDetailsService ) /** * 支持 password 模式 */ .authenticationManager( authenticationManager ) .tokenEnhancer( enhancerChain ) .accessTokenConverter( jwtAccessTokenConverter ); }
3.6.4.2 測試
再次請求 token ,返回內容中多了個剛剛加入的 jwt-ext 字段
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MTc0NTE3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhNDU1MWQ5ZS1iN2VkLTQ3NTktYjJmMS1mMGI5YjIxY2E0MmMiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.5j4hNsVpktG2iKxNqR-q1rfcnhlyV3M6HUBx5cd6PiQ", "token_type": "bearer", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImE0NTUxZDllLWI3ZWQtNDc1OS1iMmYxLWYwYjliMjFjYTQyYyIsImV4cCI6MTU3MTc3NzU3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJmNTI3ODJlOS0wOGRjLTQ2NGUtYmJhYy03OTMwNzYwYmZiZjciLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.UQMf140CG8U0eWh08nGlctpIye9iJ7p2i6NYHkGAwhY", "expires_in": 3599, "scope": "all", "jwt-ext": "JWT 擴展信息", "jti": "a4551d9e-b7ed-4759-b2f1-f0b9b21ca42c" }
3.6.4 用戶客戶端解析 JWT 數據
咱們若是在 JWT 中加入了額外信息,這些信息咱們可能會用到,而在接收到 JWT 格式的 token 以後,用戶客戶端要把 JWT 解析出來。
引入 JWT 包
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
加一個 RESTful 接口,在其中解析 JWT
@GetMapping(value = "jwt") @PreAuthorize("hasAnyRole('ROLE_ADMIN')") public Object jwtParser(Authentication authentication){ authentication.getCredentials(); OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails(); String jwtToken = details.getTokenValue(); Claims claims = Jwts.parser() .setSigningKey("dev".getBytes(StandardCharsets.UTF_8)) .parseClaimsJws(jwtToken) .getBody(); return claims; }
一樣注意其中籤名的設置要與認證服務端相同
測試
用上一步的 token 請求上面的接口
返回內容以下:
{ "user_name": "admin", "jwt-ext": "JWT 擴展信息", "scope": [ "all" ], "exp": 1571745178, "authorities": [ "ROLE_ADMIN" ], "jti": "a4551d9e-b7ed-4759-b2f1-f0b9b21ca42c", "client_id": "user-client" }
關注公衆號,有更多好玩的等着你!!!