一. Spring Security OAuth簡介
??上一章的開發(fā)l是以第三方應(yīng)用角色為中心進(jìn)行開發(fā)的鸯乃,我們做的工作就是如何實(shí)現(xiàn)第三方應(yīng)用斩萌,然后獲取服務(wù)提供者的訪問權(quán)限去訪問服務(wù)提供者的一些受保護(hù)的資源。而在這一章,我們要使用spring security oauth基于服務(wù)提供者進(jìn)行開發(fā),學(xué)習(xí)如何向其他客戶端提供令牌呻顽、并且可以驗(yàn)證這些令牌然后返回我們受保護(hù)的資源。
??服務(wù)提供商事實(shí)上包括兩種角色:授權(quán)服務(wù)器和資源服務(wù)器丹墨,它們可以處于同一個應(yīng)用程序中廊遍,當(dāng)然多個資源服務(wù)器也可以共享同一個授權(quán)服務(wù)器。
??spring social oauth的主要流程是:第三方應(yīng)用向認(rèn)證服務(wù)器獲取授權(quán)贩挣,認(rèn)證服務(wù)器向應(yīng)用發(fā)送token喉前,第三方應(yīng)用使用該token從資源服務(wù)器中獲取數(shù)據(jù)。
??spring social oauth默認(rèn)幫我們實(shí)現(xiàn)了四種傳統(tǒng)的授權(quán)模式的認(rèn)證邏輯王财,我們需要做的只是將我們自定義的認(rèn)證邏輯添加到認(rèn)證服務(wù)器中卵迂,使得我們的認(rèn)證服務(wù)器可以支持用戶名和密碼、手機(jī)號和第三方登錄的方式生成token绒净。
二. 服務(wù)提供者的簡單實(shí)現(xiàn)
@Configuration
@EnableAuthorizationServer //該注解:表明當(dāng)前是一個認(rèn)證服務(wù)器
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("imooc") //第三方應(yīng)用的標(biāo)識:比如慕課微信助手见咒,標(biāo)識哪個第三方應(yīng)用從服務(wù)提供者獲取權(quán)限
.secret(passwordEncoder().encode("imoocsecret"))
.redirectUris("http://www.baidu.com")
.authorizedGrantTypes("authorization_code","refresh_token")
.scopes("all");
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
@Component
public class AuthUserDetailService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private PasswordEncoder passwordEncoder;
// 服務(wù)提供者的哪個用戶進(jìn)行授權(quán)挂疆,比如微信中是哪個用戶賦予第三方應(yīng)用權(quán)限
@Override
public UserDetails loadUserByUsername(String username) {
logger.info("用戶{}請求授權(quán)", username);
//注意:一定要包括"role_user"角色
return new User(username, passwordEncoder.encode("123456"), AuthorityUtils.createAuthorityList("ROLE_USER"));
}
}
@Configuration
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and()
.authorizeRequests().anyRequest().authenticated().and()
.csrf().disable();
}
}
??以上配置就完成了一個簡單的認(rèn)證服務(wù)器改览,我們可以測試一下根據(jù)傳統(tǒng)的四種授權(quán)模式是否可以訪問到我們想要的資源牛曹。
??1. 授權(quán)碼模式:
??(1)獲取授權(quán)碼
????a :訪問改地址假丧,輸入用戶名和密碼之后會跳往授權(quán)頁面:http://localhost:8989/oauth/authorize?response_type=code&client_id=imooc&redirect_uri=http://www.baidu.com&scope=all
???? b:點(diǎn)擊授權(quán)之后獲取授權(quán)碼
??(2)根據(jù)授權(quán)碼獲取Token(可以使用谷歌插件Restlet Client工具):
??發(fā)送POST
請求到:http://localhost:8989/oauth/token
????a. 請求頭的設(shè)置:
????b:請求體要根據(jù)Oauth2協(xié)議規(guī)定的參數(shù)進(jìn)行添加
??認(rèn)證服務(wù)器會返回我們需要的token。
??2. 密碼模式:如果想使用這種模式荚坞,我們需要添加一些代碼
1. AppSecurityConfig類中墨闲,需要覆蓋一個方法今妄,將AuthenticationManager對象交給容器管理
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
2. ImoocAuthorizationServerConfig類中郑口,需要將AuthenticationManager設(shè)置到AuthorizationServerEndpointsConfigurer中
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
3. ImoocAuthorizationServerConfig類中鸳碧,需要給該應(yīng)用添加密碼授權(quán)模式
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("imooc")
.secret(passwordEncoder().encode("imoocsecret"))
.redirectUris("http://www.baidu.com")
.authorizedGrantTypes("authorization_code","password","refresh_token")
.scopes("all");
}
??密碼模式的請求頭和授權(quán)碼模式相同盾鳞,參數(shù)根據(jù)OAuth2協(xié)議規(guī)定參數(shù)進(jìn)行添加。
??在獲取到對應(yīng)的token之后瞻离,我們就可以使用該token從資源服務(wù)器中獲取受保護(hù)的資源了腾仅,資源服務(wù)器的配置非常簡單只需要加一個注解即可。
@Configuration
@EnableResourceServer
public class ImoocResourceServerConfig {
}
??然后我們就可以在請求頭添加該token訪問資源服務(wù)器中的資源套利。
請求頭中需要添加 : Authorization=token_type access_token
三. SpringSecurityOauth核心源碼解析
??最好可以邊看源碼邊借鑒上圖推励,梳理類之間的關(guān)系,對 token的生成的流程有一個大概的認(rèn)知肉迫,因?yàn)槲覀冃枰砑幼远x的生成邏輯验辞,我們需要知道可以從哪里入手,代碼如下:
TokenEndpoint類:
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
// 作為受保護(hù)的資源喊衫,當(dāng)程序運(yùn)行到這里跌造,說明已經(jīng)完成了第三方應(yīng)用的認(rèn)證工作
//principal包含了第三方應(yīng)用的所有信息(比如慕課微信助手的clientid、clientSecret等等)族购,說明了是哪個應(yīng)用需要獲取 tokne信息
//parameters:獲取token的請求所攜帶的參數(shù)
//1. 首先根據(jù) ClientDetailsService 獲取第三方應(yīng)用的所有信息壳贪,clientDetails中包括clientId,clientSecret寝杖,scope违施,grantType等等。
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
//2. 然后使用 DefaultOAuth2RequestFactory根據(jù) 請求參數(shù)和 clientDetail封裝TokenRequest對象
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
//3. 驗(yàn)證請求token中的scope是否有效瑟幕,是否超過了該應(yīng)用的授權(quán)范圍
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
//4. 如果是授權(quán)碼模式磕蒲,將請求參數(shù)中的scope設(shè)置為空,因?yàn)槭跈?quán)碼模式 token中的scope已經(jīng)在獲取授權(quán)碼時確定了收苏,所以token的scope應(yīng)該從code中獲取
if (isAuthCodeRequest(parameters)) {
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
//5. 如果是刷新令牌的請求亿卤,新token中的scope應(yīng)該和refresh_token中的一致
if (isRefreshTokenRequest(parameters)) {
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
//6. 獲取token
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type");
}
return getResponse(token);
}
??有上述代碼可知,主要的邏輯都在第六步中鹿霸,我們首先看下TokenGranter
類排吴,spring security oauth對該類的是這樣描述的:
Interface for granters of access tokens. Various grant types are defined in the specification, and each of those has an implementation, leaving room for extensions to the specification as needed.
??大致的意意思:該類是一個授權(quán)接口,每種授權(quán)類型都應(yīng)該實(shí)現(xiàn)該類定義自己的實(shí)現(xiàn)(加上refresh_token懦鼠,目前包括五種授權(quán)類型钻哩,所以至少有五個實(shí)現(xiàn)類)。所以如果我們想要自定義我們的認(rèn)證邏輯肛冶,需要實(shí)現(xiàn)該類街氢。
??該類的作用將TokenRequest封裝成我們想要的token對象。debug結(jié)果顯示睦袖,珊肃,目前的確包括五種授權(quán)模式,由于密碼模式比較簡單,所以我們跟著密碼模式的流程走一遍:
??spring security oauth使用裝飾者模式伦乔,將這五個類封裝到CompositeTokenGranter
中厉亏,由它根據(jù)授權(quán)類型自動選擇對應(yīng)的TokenGranter
。
@Deprecated
public class CompositeTokenGranter implements TokenGranter {
private final List<TokenGranter> tokenGranters;
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
for (TokenGranter granter : tokenGranters) {
OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
if (grant!=null) {
return grant;
}
}
return null;
}
......
}
??在調(diào)用ClientCredentialsTokenGranter
的方法之前烈和,會調(diào)用父類AbstractTokenGranter
的grant()
方法:
1. 密碼授權(quán)模式的方法:該方法沒有做什么主要的工作爱只,就是對父類返回token做了一層封裝,因?yàn)槟承┣闆r下密碼模式不應(yīng)該返回refresh_token
@Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
OAuth2AccessToken token = super.grant(grantType, tokenRequest);
if (token != null) {
DefaultOAuth2AccessToken norefresh = new DefaultOAuth2AccessToken(token);
if (!allowRefresh) {
norefresh.setRefreshToken(null);
}
token = norefresh;
}
return token;
}
2. 父類的方法:使用 ClientDetailsService 對象獲取 clientDetails 對象招刹,然后和 tokenRequest 封裝成token 對象
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
return getAccessToken(client, tokenRequest);
}
3. 調(diào)用TokenServices 對象將 clientDetail 和 TokenRequest 封裝成 結(jié)果token 對象
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
4. 首先使用 DefaultOAuth2RequestFactory 對象將 clientDetail 和 tokenRequest 封裝成 OAuth2Authentication 對象
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, null);
}
createOAuth2Request方法如下恬试,使用 TokenRequest 和 client 封裝 OAuth2Request 對象,此時tokenRequest就包含了 client對象的一些細(xì)節(jié)
public OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest) {
return tokenRequest.createOAuth2Request(client);
}
5. 然后調(diào)用 DefaultTokenServices 類的 createAccessToken() 方法根據(jù) OAuth2Authentication創(chuàng)建結(jié)果 Token 對象
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
//1. 首先根據(jù)認(rèn)證信息疯暑,從tokenStore中查詢是否給該用戶返回給Token训柴, 這就是為什么我們測試密碼模式和授權(quán)碼模式時返回的token是相同的
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
//(1)如果Token已過期,則將該token從 tokenStore中移除妇拯,然后重新創(chuàng)建 token
if (existingAccessToken.isExpired()) {
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
//(2)重新存儲一下 token畦粮,因?yàn)檎J(rèn)證信息有可能改變
else {
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
//2. 創(chuàng)建 token ,然后將 token和refresh_token存儲起來
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
6. 根據(jù)認(rèn)證信息和 refresh_token 創(chuàng)建 Token 對象
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
//1. 創(chuàng)建 DefaultOAuth2AccessToken ,然后設(shè)置默認(rèn)值
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
//2. 使用 token增強(qiáng)器 accessTokenEnhancer給 token添加附加信息乖阵,我們可以提供自定義 token增強(qiáng)器給token添加自定義的信息宣赔。
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
四. 重構(gòu)代碼
??如果你指定了成功處理器,那么無論使用哪種方式進(jìn)行認(rèn)證瞪浸,認(rèn)證完成之后都會跳往成功處理器進(jìn)行處理儒将,所以我們只需要原來成功處理器中的代碼修改成 向客戶端返回 token 的邏輯就可以了。
??所以我們按照以上圖片的流程對代碼進(jìn)行重構(gòu)
1. 首先在認(rèn)證服務(wù)器中做一些基本的配置对蒲,將成功處理器添加到流程中
@Configuration
@EnableResourceServer
public class ImoocResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
http.formLogin().successHandler(authenticationSuccessHandler).and()
.authorizeRequests().anyRequest().authenticated().and()
.csrf().disable();
}
}
2. 成功處理器:根據(jù)之前源碼分析流程構(gòu)建 token 對象
@Component
public class ImoocSuccessHandler implements AuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Autowired(required = false)
private TokenEnhancer tokenEnhancer;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 1. BasicAuthenticationFilter類有獲取頭信息代碼钩蚊,可以借鑒
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Basic ")) {
throw new RuntimeException("請求的格式不正確");
}
String[] tokens = extractAndDecodeHeader(header, request);
String clientId = tokens[0];
String clientSecret = tokens[1];
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if(clientDetails == null){
logger.info("{}對應(yīng)的信息不存在!", clientId);
throw new RuntimeException("無效的clientId");
}
if( !passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())){
throw new RuntimeException("無效的clientSecret");
}
// 根據(jù)之前分析流程獲取最終結(jié)果
TokenRequest tokenRequest = new TokenRequest(null, clientId, clientDetails.getScope(), "custom");
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
OAuth2AccessToken accessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
accessToken = tokenEnhancer == null? accessToken:tokenEnhancer.enhance(accessToken, oAuth2Authentication);
logger.info("token為:{}", accessToken);
}
private String[] extractAndDecodeHeader (String header, HttpServletRequest request) throws
UnsupportedEncodingException {
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
} catch (IllegalArgumentException e) {
throw new BadCredentialsException(
"Failed to decode basic authentication token");
}
String token = new String(decoded, Charset.forName("UTF-8"));
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
return new String[]{token.substring(0, delim), token.substring(delim + 1)};
}
}
??以上的配置就算完成了用戶名和密碼登錄的基本授權(quán)流程,我們可以做一些測試:
?? (1)發(fā)送請求獲取 token蹈矮, 注意請求頭和之前相同砰逻。
??(2)根據(jù) token 獲取資源
五. Token相關(guān)
??1. TokenStore配置:默認(rèn)情況下,服務(wù)提供者的 token 是存儲在內(nèi)存中的泛鸟,當(dāng)服務(wù)重啟時蝠咆,發(fā)出去的 token 將變得無效,因?yàn)閼?yīng)用無法在內(nèi)存中獲取 token對應(yīng)的認(rèn)證信息北滥,所以可以將 token保存到持久化的介質(zhì)中刚操,以保證當(dāng)某些特殊情況機(jī)器導(dǎo)致重啟時發(fā)出去的 token 依然有效。
1. TokenStore配置類
@Configuration
public class TokenStoreConfig {
@Bean
public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory){
return new RedisTokenStore(redisConnectionFactory);
}
}
2. 通過授權(quán)服務(wù)器中的配置再芋, 將 tokenStore加到 入口類中
@Configuration
@EnableAuthorizationServer
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore)
......
}
}
??2. 使用 JWT 替換 token
?? JWT ( JSON WEB TOKEN )
菊霜,主要用作交換信息和授權(quán),主要包含三個部分:Header(包括所使用的 token 類型和所使用的簽名算法)济赎、Payload(主要數(shù)據(jù)鉴逞,jwt 存儲數(shù)據(jù)的部分)记某,Signature(簽名算法,判斷數(shù)據(jù)的有效性(是否發(fā)生被惡意篡改构捡、丟失等改變))辙纬。
?? spring security oauth中的token中包含的數(shù)據(jù)是沒有任何業(yè)務(wù)含義的,它的作用主要是保證了自身在持久化介質(zhì)中的唯一性叭喜,可以根據(jù) token從介質(zhì)中可以獲取到用戶認(rèn)證的信息,它的初始化方式是 DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
蓖谢,而JWT不需要依賴任何持久化介質(zhì)捂蕴,用戶的身份信息是存儲在 token中的,所以它也不存在由于服務(wù)重啟導(dǎo)致token變得無效的問題闪幽。
@Configuration
public class TokenStoreConfig {
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("imooc"); //使用密簽加密和解密 token
return jwtAccessTokenConverter;
}
/**
* 添加附加信息
* */
@Bean
public TokenEnhancer tokenEnhancer(){
return new TokenEnhancer() {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> info = new HashMap<>();
info.put("鍵", "自定義數(shù)據(jù)");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
};
}
}
@Configuration
@EnableAuthorizationServer
public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenEnhancer tokenEnhancer;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> chain = new ArrayList<>();
chain.add(tokenEnhancer);
chain.add(jwtAccessTokenConverter);
tokenEnhancerChain.setTokenEnhancers(chain);
endpoints
.tokenStore(tokenStore)
.tokenEnhancer(tokenEnhancerChain)
......
}
}
??將 OauthToken 替換為 JWT的配置就完成了啥辨,我們可以做一些測試:
??(1)獲取 token:有以下結(jié)果可知,的確已經(jīng)替換成功盯腌,不再是32位的UUID字符串溉知,而是包含三個部分的 JWT,每個部分由"." 隔開腕够。
??(2)訪問資源:將生成的 token 添加到請求頭中级乍,獲取到了受保護(hù)的資源
??如果想要查看我們生成的 JWT 包含了什么信息,可以點(diǎn)擊該網(wǎng)址查看帚湘。
?? SpringSecurity沒有給我們提供可以獲取 JWT 中自定義的數(shù)據(jù)的途徑玫荣,所以我們需要自定義接口來解析 JWT獲取我們想要的數(shù)據(jù)。
1.添加依賴
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
2. 解析 token
@GetMapping("/me")
public Object getMe(HttpServletRequest request){
String authorization = request.getHeader("Authorization");
String token = StringUtils.substringAfter(authorization, "bearer ");
Claims claims = Jwts.parser().setSigningKey("imooc".getBytes(Charset.forName("UTF-8"))).parseClaimsJws(token).getBody();
return claims;
}
六. 使用 JWT 實(shí)現(xiàn) SSO
單點(diǎn)登錄(SingleSignOn大诸,SSO)捅厂,就是通過用戶的一次性鑒別登錄。當(dāng)用戶在身份認(rèn)證服務(wù)器上登錄一次以后资柔,即可獲得訪問單點(diǎn)登錄系統(tǒng)中其他關(guān)聯(lián)系統(tǒng)和應(yīng)用軟件的權(quán)限焙贷,同時這種實(shí)現(xiàn)是不需要管理員對用戶的登錄狀態(tài)或其他信息進(jìn)行修改的,這意味著在多個應(yīng)用系統(tǒng)中贿堰,用戶只需一次登錄就可以訪問所有相互信任的應(yīng)用系統(tǒng)辙芍。這種方式減少了由登錄產(chǎn)生的時間消耗,輔助了用戶管理羹与,是目前比較流行的
??使用三個系統(tǒng):應(yīng)用A沸手、應(yīng)用B、認(rèn)證服務(wù)器注簿,進(jìn)行模擬單點(diǎn)登錄契吉,當(dāng)任何一個應(yīng)用經(jīng)過認(rèn)證服務(wù)器的認(rèn)證和授權(quán)之后,其他的應(yīng)用都無需進(jìn)行認(rèn)證直接可以向認(rèn)證服務(wù)器請求授權(quán)诡渴,并根據(jù)所授予的權(quán)限訪問資源服務(wù)器中的資源捐晶。
??所以我們需要搭建三個項(xiàng)目菲语, APP1、APP2惑灵、APPServer
??(1)首先創(chuàng)建三個項(xiàng)目的父項(xiàng)目:sso-demo山上,所導(dǎo)的依賴和之前的父項(xiàng)目相同
??(2)創(chuàng)建認(rèn)證服務(wù)器
1. 添加依賴,注意這三個項(xiàng)目的依賴相同
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
</dependencies>
2. 服務(wù)端的一些配置
@Configuration
public class SsoServerConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and()
.authorizeRequests().anyRequest().authenticated()
.and().csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new PasswordEncoder() {
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
public boolean matches(CharSequence charSequence, String s) {
return StringUtils.equals(charSequence, s);
}
};
}
}
3. 認(rèn)證服務(wù)器類的一些配置
@Configuration
@EnableAuthorizationServer
public class SsoAuthrizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("app1")
.secret("appsecret1")
.authorizedGrantTypes("authorization_code", "refresh_token")
.redirectUris("http://localhost:9091/app1/login")
.scopes("all")
.autoApprove(true) //自動授權(quán)
.and()
.withClient("app2")
.secret("appsecret2")
.authorizedGrantTypes("authorization_code", "refresh_token")
.redirectUris("http://localhost:9092/app2/login")
.scopes("all")
.autoApprove(true);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(jwtAccessTokenConverter());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()"); //表示用戶經(jīng)過身份認(rèn)證之后英支,才可以訪問 tokenkey
}
// JWT 的一些配置
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("imooc");
return jwtAccessTokenConverter;
}
}
4. 端口和系統(tǒng)用戶的一些配置
server.port=9090
server.servlet.context-path=/server
spring.security.user.password=123456
??(2)創(chuàng)建客戶端應(yīng)用(兩個客戶端的配置基本相同)
1. 客戶端應(yīng)用的配置類
@SpringBootApplication
@EnableOAuth2Sso //開啟單點(diǎn)登錄功能
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
2. 客戶端應(yīng)用的配置文件
server.port=9092
server.servlet.context-path=/app2
security.oauth2.client.client-secret=appsecret2
security.oauth2.client.client-id=app2
#用戶認(rèn)證的地址
security.oauth2.client.user-authorization-uri=http://localhost:9090/server/oauth/authorize
#用戶獲取token的地址
security.oauth2.client.access-token-uri=http://localhost:9090/server/oauth/token
#由于配置了 tokenKey是需要已認(rèn)證的用戶才可以訪問佩憾,所以配置用戶獲取tokenkey的地址
security.oauth2.resource.jwt.key-uri=http://localhost:9090/server/oauth/token_key
3. 測試使用的前臺頁面
<h1>客戶端2</h1>
<a href="http://localhost:9091/app1/sso1demo.html">跳轉(zhuǎn)到應(yīng)用1</a>
??由于我們設(shè)置的是自動授權(quán),所以不會顯示授權(quán)頁面(可以選擇確認(rèn)授權(quán)或者拒絕授權(quán))干花,默認(rèn)的授權(quán)頁面是由WhitelabelApprovalEndpoint
類提供的妄帘,如果想要自定義授權(quán)頁面,只需要仿照該類自定義即可池凄。
1. controller類
@RestController
@SessionAttributes("authorizationRequest")
public class GrantPage{
@Autowired
private ObjectMapper objectMapper;
@RequestMapping("/oauth/confirm_access")
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) {
// model中包含了授權(quán)請求中的所有信息抡驼,包括 回調(diào)地址、授權(quán)范圍 Scope等等肿仑,你可以自定義將某些信息顯示到頁面中
//比如提示用 當(dāng)前哪個應(yīng)用正在請求授權(quán)致盟、授權(quán)范圍等等
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("/base_grant.html");
return modelAndView;
}
}
2. 前臺頁面
<h2>自定義授權(quán)頁面</h2>
//注意兩個表單提交地址是相同的 /oauth/authorize,由于這里我存在相對路徑問題所以我寫成了絕對路徑
// 判斷用戶是否授權(quán)是根據(jù)user_oauth_approval的值來判斷的尤慰,具體邏輯在`AuthorizationEndpoint`類中馏锡,想看的可以看下
<form method="post" action="http://localhost:9090/server/oauth/authorize">
<input name="user_oauth_approval" value="true" type="hidden"/>
<button class="btn" type="submit"> 同意授權(quán)</button>
</form>
<form method="post" action="http://localhost:9090/server/oauth/authorize">
<input name="user_oauth_approval" value="false" type="hidden"/>
<button class="btn" type="submit"> 拒絕授權(quán)</button>
</form>