springboot2.0整合OAuth2並使用JWT作為token。
之前實現了Springboot之Security前後端分離登入 剛好這段時間有空,乘機整合下OAuth2。記錄下當中遇到的問題和處理方式。
什麼是OAuth2?
具體程式碼實現
POM檔案
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
授權伺服器
@Configuration
@EnableAuthorizationServer
public class OAuth2ServerConfiguration extends AuthorizationServerConfigurerAdapter {
private static final String CLIENT_ID = "client"; //客戶端
private static final String CLIENT_SECRET = "123456"; //secret客戶端安全碼
private static final String GRANT_TYPE_PASSWORD = "password"; // 密碼模式授權模式
private static final String AUTHORIZATION_CODE = "authorization_code"; //授權碼模式 授權碼模式使用到了回撥地址,是最為複雜的方式,通常網站中經常出現的微博,qq第三方登入,都會採用這個形式。
private static final String REFRESH_TOKEN = "refresh_token"; //
private static final String IMPLICIT = "implicit"; //簡化授權模式
private static final String GRANT_TYPE = "client_credentials"; //客戶端模式
private static final String SCOPE_WEB = "web"; //授權範圍 web端
private static final String SCOPE_IOS = "ios"; //授權範圍 ios端
private static final String SCOPE_ANDROID = "android";
private static final String SCOPE_BOOT = "boot"; //授權範圍 專案名稱
private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 30*24*60*60; //token 有效時間 一個月
private static final int REFRESH_TOKEN_VALIDITY_SECONDS = 30*24*60*60; //重新整理token有效時間 一個月
/**
* 描述:注入密碼加密編碼器 進行密碼加密
*/
@Autowired
BCryptPasswordEncoder passwordEncoder;
/**
* 描述:注入使用者資訊處理類 處理使用者賬號資訊
*/
@Autowired
UserDetailsServiceImpl userDetailService;
/**
* 描述:注入token生成器 處理token的生成方式
*/
@Autowired
TokenStore tokenStore;
/**
* 描述: 注入AuthenticationManager管理器
*/
@Autowired
AuthenticationManager authenticationManager;
/**
* 描述: 注入jwtAccessTokenConverter 增強token
*/
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
String secret = new BCryptPasswordEncoder().encode(CLIENT_SECRET); // 用 BCrypt 對密碼編碼
//配置客戶端資訊
clients.inMemory() // 使用in-memory儲存
.withClient(CLIENT_ID) //client_id用來標識客戶的Id
.authorizedGrantTypes(AUTHORIZATION_CODE,GRANT_TYPE, REFRESH_TOKEN,GRANT_TYPE_PASSWORD,IMPLICIT) //允許授權型別
.scopes(SCOPE_WEB,SCOPE_IOS,SCOPE_ANDROID,SCOPE_BOOT) //允許授權範圍
.authorities("ROLE_CLIENT") //客戶端可以使用的許可權
.secret(secret) //secret客戶端安全碼
.autoApprove(true) // 為true 則不會被重定向到授權的頁面,也不需要手動給請求授權,直接自動授權成功返回code
.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS) //token 時間秒
.refreshTokenValiditySeconds(REFRESH_TOKEN_VALIDITY_SECONDS);//重新整理token 時間 秒
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 允許表單登入
.allowFormAuthenticationForClients()
// 密碼加密編碼器
.passwordEncoder(passwordEncoder)
// 允許所有的checkToken請求
.checkTokenAccess("permitAll()");
}
/**
* 配置令牌
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
// 認證管理器 - 在密碼模式必須配置
.authenticationManager(authenticationManager)
// 自定義校驗使用者service
.userDetailsService(userDetailService)
// 是否能重複使用 refresh_token
.reuseRefreshTokens(false);
// 設定令牌增強 JWT 轉換
TokenEnhancerChain enhancer = new TokenEnhancerChain();
enhancer.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
endpoints.tokenEnhancer(enhancer);
}
}
資源伺服器
@Configuration
@EnableResourceServer
public class OAuth2ResourceConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).and().csrf().disable();
// 配置不登入可以訪問 - 放行路徑配置
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
.authorizeRequests();
registry.antMatchers("/login","/oauth/**").permitAll();
registry.anyRequest().authenticated();
}
}
token處理
@Configuration
public class TokenConfig {
/** JWT金鑰 */
private String signingKey = "fastboot";
/**
* JWT 令牌轉換器
* @return
*/
@Bean("jwtAccessTokenConverter")
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwt = new JwtAccessTokenConverter(){
/**
* 使用者資訊JWT加密
*/
@Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
UserInfo user = (UserInfo) authentication.getUserAuthentication().getPrincipal();
Set<String> tokenScope = token.getScope();
String scopeTemp = " ";
if(tokenScope!=null&&tokenScope.size()>0){
scopeTemp=tokenScope.iterator().next();
}
String scope =scopeTemp;
//將額外的引數資訊存入,用於生成token
Map<String, Object> data = new HashMap<String, Object>(4){{
put("userId", user.getUserId());
put("username", user.getUsername());
put("email", user.getEmail());
put("roleDtos",user.getRoleDtos());
put("nickName", user.getNickName());
put("authorities", user.getAuthorities());
put("scope",scope);
}};
//自定義TOKEN包含的資訊
token.setAdditionalInformation(data);
return super.encode(accessToken, authentication);
}
/**
* 使用者資訊JWT
*/
@Override
protected Map<String, Object> decode(String token) {
//解析請求當中的token 可以在解析後的map當中獲取到上面加密的資料資訊
Map<String, Object> decode = super.decode(token);
Long userId = (Long)decode.get("userId");
String username = (String)decode.get("username");
String email = (String)decode.get("email");
String nickName = (String)decode.get("nickName");
String scope = (String)decode.get("scope");
List<GrantedAuthority> grantedAuthorityList=new ArrayList<>();
//注意這裡獲取到的許可權 雖然資料庫存的許可權是 "sys:menu:add" 但是這裡就變成了"{authority=sys:menu:add}" 所以使用@PreAuthorize("hasAuthority('{authority=sys:menu:add}')")
List<LinkedHashMap<String,String>> authorities =(List<LinkedHashMap<String,String>>) decode.get("authorities");
for (LinkedHashMap<String, String> authority : authorities) {
SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(authority.getOrDefault("authority", "N/A"));
grantedAuthorityList.add(grantedAuthority);
}
UserInfo userInfo =new UserInfo(username,"N/A",userId, grantedAuthorityList);
userInfo.setNickName(nickName);
userInfo.setEmail(email);
//需要將解析出來的使用者存入全域性當中,不然無法轉換成自定義的user類
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userInfo,null, grantedAuthorityList);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
decode.put("user_name",userInfo);
return decode;
}
};
jwt.setSigningKey(signingKey);
return jwt;
}
/**
* 配置 token 如何生成
* 1. InMemoryTokenStore 基於記憶體儲存
* 2. JdbcTokenStore 基於資料庫儲存
* 3. JwtTokenStore 使用 JWT 儲存 該方式可以讓資源伺服器自己校驗令牌的有效性而不必遠端連線認證伺服器再進行認證
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
public String getSigningKey() {
return signingKey;
}
public void setSigningKey(String signingKey) {
this.signingKey = signingKey;
}
}
controller介面
@RestController
@RequestMapping("/oauth")
public class OauthController {
@Autowired
TokenEndpoint tokenEndpoint;
@PostMapping(value = "/token")
public ResultInfo<OAuth2AccessToken> token(Principal principal, @RequestParam Map<String, String> parameters) throws Exception {
ResponseEntity<OAuth2AccessToken> accessToken = tokenEndpoint.postAccessToken(principal, parameters);
OAuth2AccessToken token = accessToken.getBody();
// TODO 可以考慮將返回的TOKEN資訊存入redis或者資料庫
return ResultInfo.success(token);
}
@PostMapping("/t1")
@PreAuthorize("hasAuthority('{authority=sys:menu:add}')")
public String getDemo(String name){
if(SecurityContextHolder.getContext() == null) {
return null;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserInfo userInfo = (UserInfo) authentication.getPrincipal();
// return ResultInfo.success(userInfo);
return userInfo.toString();
}
}
上面基本上就是完整的程式碼了。其他的類如:UserDetailsServiceImpl,UserInfo 略!!!
遇到的問題
1 token過期時間設定 可以在OAuth2ServerConfiguration 當中設定accessTokenValiditySeconds(秒) 也可以在TokenConfig 裡面進行jwt加密的時候進行設定,token.setExpiration(); 設定後會覆蓋OAuth2ServerConfiguration 當中的。
2 許可權不匹配問題 雖然資料庫存的許可權是 “sys:menu:add” 但是oauth2取的時候變成了"{authority=sys:menu:add}" 所以使用介面上使用@PreAuthorize(“hasAuthority(’{authority=sys:menu:add}’)”)進行許可權匹配。
3 serurity的User類無法轉換為自定義的user子類的問題,需要在 JWT解密的時候,重新構建然後存入全域性當中。(PS:無法在獲取token當中獲取到自定義的user子類)
相關文章
- JWT 快速為使用者生成 tokenJWT
- SpringBoot 整合 JWT 實現 token 驗證,token 登出Spring BootJWT
- Spring Boot中使用token:jwtSpring BootJWT
- JWT TokenJWT
- Composer 使用 JWT 生成 TOKEN 例項JWT
- spring security oauth2搭建resource-server demo及token改造成JWT令牌SpringOAuthServerJWT
- Spring Security原始碼分析十一:Spring Security OAuth2整合JWTSpring原始碼OAuthJWT
- 如何在SpringBoot中整合JWT(JSON Web Token)鑑權Spring BootJWTJSONWeb
- Spring Cloud Security:Oauth2結合JWT使用SpringCloudOAuthJWT
- JWT(Json WEB Token)JWTJSONWeb
- springboot2.0 整合springDataJpaSpring Boot
- springboot2.0整合webserviceSpring BootWeb
- springboot2.0整合rabbitmqSpring BootMQ
- SpringBoot2.0 整合 springmvcSpring BootSpringMVC
- Spring Boot使用JWT進行token驗證Spring BootJWT
- jwt生成token和token解析基礎JWT
- SpringBoot2.0應用(五):SpringBoot2.0整合MyBatisSpring BootMyBatis
- SpringBoot2.0應用(二):SpringBoot2.0整合ActiveMQSpring BootMQ
- SpringBoot2.0應用(三):SpringBoot2.0整合RabbitMQSpring BootMQ
- jwt生成token報錯JWT
- 038.CI4框架CodeIgniter,使用Jwt生成token框架JWT
- 還不會使用JWT格式化OAuth2令牌嗎?JWTOAuth
- JSON Web Token(JWT) 簡介JSONWebJWT
- token 會話設計 (JWT)會話JWT
- Springboot 整合 SpringCache 使用 Redis 作為快取Spring BootGCRedis快取
- 前後端分離之JWT(JSON Web Token)的使用後端JWTJSONWeb
- jwt token 重新整理問題JWT
- asp.net core使用identity+jwt保護你的webapi(二)——獲取jwt tokenASP.NETIDEJWTWebAPI
- 基於 JWT + Refresh Token 的使用者認證實踐JWT
- SpringBoot 整合SpringSecurity JWTSpring BootGseJWT
- SpringSecurity之整合JWTSpringGseJWT
- 一文搞懂Cookie,Session,Token,JWTCookieSessionJWT
- 【網路傳輸】Cookie、Session、Token、JWTCookieSessionJWT
- 一文搞懂 Cookie,Session,Token,JWTCookieSessionJWT
- vue4 + laravel8使用JWT登入及token驗證VueLaravelJWT
- springboot+jwt做api的token認證Spring BootJWTAPI
- DRF之JWT簽發Token原始碼分析JWT原始碼
- spring5.0 security oauth2 token 儲存失敗SpringOAuth