密碼模式授權相對來說比較簡單,只使用到了申請token的接口也就是/oauth/token的接口,授權碼模式相對復雜一點,以下為介紹:
1.增加AuthorizationServer配置
為了讓數(shù)據(jù)更直觀,將授權碼模式的一些存儲方式改為jdbc的
/// ### AuthorizationServerConfiguration.java
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
// 授權碼模式code存儲方式,默認內(nèi)存存儲
return new JdbcAuthorizationCodeServices(dataSource);
}
@Bean
public ApprovalStore approvalStore() {
// 授權允許存儲方式,默認內(nèi)存存儲
return new JdbcApprovalStore(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// 授權允許存儲方式
.approvalStore(approvalStore())
// 授權碼模式code存儲方式
.authorizationCodeServices(authorizationCodeServices())
// token存儲方式
.tokenStore(tokenStore())
// 使用jwt增強
.tokenEnhancer(jwtAccessTokenConverter())
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.authenticationManager(authenticationManager);
// 配置tokenServices參數(shù)
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
endpoints.tokenServices(tokenServices);
}
2.增加webSecurity的配置,由于需要訪問服務下的登陸與允許授權的頁面,需要配置permitall
@Configuration
@EnableWebSecurity
@Order(10)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
private UserServiceDetail userServiceDetail;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.parentAuthenticationManager(authenticationManagerBean())
.userDetailsService(userServiceDetail)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 登陸位置會有rememberMe選項
.rememberMe()
.and()
.authorizeRequests()
// 允許/login訪問地址
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and()
.logout()
.logoutSuccessUrl("/login")
.invalidateHttpSession(true)
.permitAll()
.and()
.csrf().disable();
}
}
3.數(shù)據(jù)庫oauth_client_details增加一條記錄
密碼是123456的bcrypt方式加密
4.開始測試
- 瀏覽器輸入地址
response_type=code: 返回的是授權碼
client_id=code: 與數(shù)據(jù)庫的clientId對應
redirect_uri=http:/baidu.com: 與數(shù)據(jù)庫web_redirect_uri對應,授權碼code獲取成功重定向的地址
state=123 任意的字符串
-
跳轉到login頁面
image.png
輸入賬號密碼賬號,重定向到redirect_uri,并且攜帶code: https://www.baidu.com/?code=vsf7NQ&state=123
- 查看數(shù)據(jù)庫
oauth_approvals 增加一條數(shù)據(jù)
oauth_code 增加一條數(shù)據(jù),code=vsf7NQ
- 通過得到的code,訪問/oauth/token進行申請token,由于之前設置了允許方法GET,POST所以這兩種請求方法都可以申請到token
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTcyOTk0NjEsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiMDUyNDdiMGItZjJiZS00MWZhLTlmODItOWRhZjU2NDZjNWI0IiwiY2xpZW50X2lkIjoiY29kZSIsInNjb3BlIjpbImFsbCJdfQ.-KDyRZyp-dWlFOk7XVnRg4EpDKzabspkzlKcr0M3tu4",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiIwNTI0N2IwYi1mMmJlLTQxZmEtOWY4Mi05ZGFmNTY0NmM1YjQiLCJleHAiOjE1NTczMDMwNjEsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjZhMWNiMmFkLWMxODEtNDcyZi1hYjIzLWY5ZjQ1MTQwMDJhMiIsImNsaWVudF9pZCI6ImNvZGUifQ.aG72owlXCaqdOTVw2NM5fALKMkWsW1Dce2zIvz_lqu4",
"expires_in": 2248,
"scope": "all",
"jti": "05247b0b-f2be-41fa-9f82-9daf5646c5b4"
}
- 再次查看oauth_code表,發(fā)現(xiàn)code=vsf7NQ的記錄被刪除了,說明授權碼模式code只能被使用一次。
接下來通過源碼來解析一下整個過程
攔截鏈
依次執(zhí)行順序為:
- WebAsyncManagerIntegrationFilter
沒做什么相關的操作
- SecurityContextPersistenceFilter
創(chuàng)建安全上下文,請求結束時,清空,同時獲取到用戶,以及認證信息
- HeaderWriterFilter
忽略
- LogoutFilter
判斷是否是登出操作;如果是,執(zhí)行登出操作
- UsernamePasswordAuthenticationFilter
判斷哪些請求需要鑒權,哪些不需要,如果是登陸的接口,就會通過authenticationManager去進行校驗,然后根據(jù)檢驗結果執(zhí)行不同的操作
- DefaultLoginPageGeneratingFilter
如果是登陸,登陸錯誤,登陸成功的請求,直接返回到頁面
boolean loginError = isErrorPage(request);
boolean logoutSuccess = isLogoutSuccess(request);
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
String loginPageHtml = generateLoginPageHtml(request, loginError,
logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
return;
}
- RequestCacheAwareFilter
判斷使用緩存的請求
- SecurityContextHolderAwareRequestFilter
忽略
- RememberMeAuthenticationFilter
RememberMe的校驗
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response);
if (rememberMeAuth != null) {
// .... 從cookie里面獲取登陸狀態(tài),成功直接結束請求,返回給用戶
}
chain.doFilter(request, response);
}
// .....
}
- AnonymousAuthenticationFilter
如果SecurityContextHolder中沒有當前請求用戶授權信息,創(chuàng)建一個匿名用戶放到全局域SecurityContextHolder中
- SessionManagementFilter
判斷session中是否已經(jīng)存在授權信息,首次請求只會獲取到上一個filter存放的匿名信息
- ExceptionTranslationFilter
這個fitler會全局處理下游拋出的異常,如果拋出的異常是AccessDeniedException,且是匿名用戶的話會跳到指定的登陸頁面
- FilterSecurityInterceptor
如果沒拋出異常,會將登陸的用戶鑒權信息存儲到SecurityContextHolder,供一次訪問使用
匿名用戶