密码模式授权相对来说比较简单,只使用到了申请token的接口也就是/oauth/token的接口,授权码模式相对复杂一点,以下为介绍:
1.增加AuthorizationServer配置
为了让数据更直观,将授权码模式的一些存储方式改为jdbc的
/// ### AuthorizationServerConfiguration.java
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
// 授权码模式code存储方式,默认内存存储
return new JdbcAuthorizationCodeServices(dataSource);
}
@Bean
public ApprovalStore approvalStore() {
// 授权允许存储方式,默认内存存储
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参数
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.数据库oauth_client_details增加一条记录
密码是123456的bcrypt方式加密
4.开始测试
- 浏览器输入地址
response_type=code: 返回的是授权码
client_id=code: 与数据库的clientId对应
redirect_uri=http:/baidu.com: 与数据库web_redirect_uri对应,授权码code获取成功重定向的地址
state=123 任意的字符串
-
跳转到login页面
image.png
输入账号密码账号,重定向到redirect_uri,并且携带code: https://www.baidu.com/?code=vsf7NQ&state=123
- 查看数据库
oauth_approvals 增加一条数据
oauth_code 增加一条数据,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表,发现code=vsf7NQ的记录被删除了,说明授权码模式code只能被使用一次。
接下来通过源码来解析一下整个过程
拦截链
依次执行顺序为:
- WebAsyncManagerIntegrationFilter
没做什么相关的操作
- SecurityContextPersistenceFilter
创建安全上下文,请求结束时,清空,同时获取到用户,以及认证信息
- HeaderWriterFilter
忽略
- LogoutFilter
判断是否是登出操作;如果是,执行登出操作
- UsernamePasswordAuthenticationFilter
判断哪些请求需要鉴权,哪些不需要,如果是登陆的接口,就会通过authenticationManager去进行校验,然后根据检验结果执行不同的操作
- 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里面获取登陆状态,成功直接结束请求,返回给用户
}
chain.doFilter(request, response);
}
// .....
}
- AnonymousAuthenticationFilter
如果SecurityContextHolder中没有当前请求用户授权信息,创建一个匿名用户放到全局域SecurityContextHolder中
- SessionManagementFilter
判断session中是否已经存在授权信息,首次请求只会获取到上一个filter存放的匿名信息
- ExceptionTranslationFilter
这个fitler会全局处理下游抛出的异常,如果抛出的异常是AccessDeniedException,且是匿名用户的话会跳到指定的登陆页面
- FilterSecurityInterceptor
如果没抛出异常,会将登陆的用户鉴权信息存储到SecurityContextHolder,供一次访问使用
匿名用户
网友评论