统一授权服务
- 1.oauth2.0授权模式介绍
- 2.授权服务搭建,资源服务配置
- 3.授权码模式自定义登陆页面和授权页面
- 4.自定义手机号+验证码授权模式
一.oauth2.0介绍
OAuth2.0提供了四种授权(获取令牌)方式,四种方式均采用不同的执行流程,让我们适应不同的场景,除此之外我们还可以通过TokenGranter扩展自定义授权模式
OAuth2.0中有四个重要角色:
Oauth2.0 角色 | 说明 |
---|---|
客户端 | 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源 |
资源拥有者 | 通常为用户,也可以是应用程序,资源的拥有者 |
授权服务 | 用户服务提供商对资源拥有的身份认证,对访问资源进行授权,认证成功后发放令牌,作为客户端访问资源的凭据 |
资源服务 | 存储资源服务器 |
1.1.授权模式
1.1.1 授权码模式 authorization_code
这种模式是四种模式中最安全的一种模式。一般用于Web服务器端应用或第三方的原生App调用资源服务的时候。
客户端需要访问资源服务需要以下七个步骤
(1)请求授权服务,获取用户授权
(2)授权服务拉起授权,请求用户进行授权
(3)用户授权通过,授权服务器通过redirect_uri转发将授权码(AuthorizationCode)转经发送给客户端
(4)客户端拿着授权码向授权服务器索要访问access_token
(5)授权服务向客户端返回access_token
(6)客户端拿着授权码请求资源服务
(7)资源服务返回请求资源
客户端请求code
http://localhost:8070/auth 是经过网关处理
http://localhost:8070/auth/oauth/token?grant_type=authorization_code&client_id=base-web&client_secret=123456&code=nnxcQj&redirect_uri=http://www.baidu.com
参数列表如下:
参数 | 说明 |
---|---|
clinet_id | 客户端接入标识 |
response_type | 授权模式固定code |
scope | 客户端获取权限范围,权限范围配置由oauth_client_details表的autoapprove 配置,多个逗号拼接,scope是其中一个或多个,多个逗号拼接 |
redirect_url | 跳转url,当授权码申请成功会跳转到此地址上,并在后边带上code参数 |
通过code获取access_token
http://localhost:8070/auth/oauth/token?grant_type=authorization_code&client_id=base-web&client_secret=123456&code=nnxcQj&redirect_uri=http://www.baidu.com
参数列表如下
参数 | 说明 |
---|---|
clinet_id | 客户端接入标识 |
client_secret | 客户端密钥 |
grant_type | 授权类型,填写authorization_code,授权码模式 |
code | 授权码,一次有效 |
redirect_url | 跳转url,和获取code时一致 |
1.1.2 .密码模式 password
密码模式使用较多,适应于第一方的单页面应用以及第一方的原生App
(1)客户将将用户名、密码发送给客户端
(2)客户端拿着用户名、密码向授权服务器请求令牌(access_token)
(3)授权服务器将access_token给客户端
(4)客户端携带access_token请求资源服务
(5)资源服务返回请求资源给客户端
获取token
http://localhost:8070/auth/oauth/token?grant_type=password&username=test123&password=123456
另外请求头添加
#YWRtaW46MTIzNDU2 字符串为clinet:client_secret 在base64加密后的字符串
authorization:Basic YWRtaW46MTIzNDU2
参数 | 说明 |
---|---|
clinet_id | 客户端接入标识 |
client_secret | 客户端密钥 |
grant_type | 授权类型,填写authorization_code,授权码模式 |
username | 资源拥有者用户名 |
password | 用户名密码 |
发送给client 这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于client是 我们自己开发的情况下。
1.1.3 .简化模式
(1)用户打开客户端,客户端携带clientId请求授权服务
(2)授权服务重定向到客户端让用户进行授权
(3)用户输入账号密码进行授权
(4)用户授权通过,重定向到redirect_uri并携带token
(5)携带token请求资源服务
(6)资源服务返回请求资源
http://192.168.31.234:8070/auth/oauth/authorize?response_type=token&client_id=admin&redirect_uri=http://www.baidu.com&scope=read&approved=true
参数列表如下:
参数 | 说明 |
---|---|
clinet_id | 客户端接入标识 |
response_type | 简化模式固定为token |
scope | 客户端权限 |
redirect_url | 跳转url,当授权码申请成功后会跳转到此地址,并带上后面的code参数 |
approved | 为true时,用户输入账号密码后直接重定向返回token,false需要用户手动授权 |
1.1.4 .客户端模式
(1)用户访问客户端
(2)客户端携带clientId和client_secret 访问授权服务
(3)授权服务返回token
(4)客户端携带token请求资源服务
(5)资源服务返回请求资源
http://localhost:8070/auth/oauth/token?grant_type=client_credentials&client_id=base-web&client_secret=123456&scope=read
参数列表如下:
参数 | 说明 |
---|---|
clinet_id | 客户端接入标识 |
clinet_secret | 客户端密钥 |
grant_type | 授权类型,填写clinet_credentials表示客户端模式 |
scope | 授权范围 |
二.授权服务搭建
2.1 创建oauth2.0需要表结构
oauth_client_details表
参数 | 类型 | 说明 |
---|---|---|
client_id | varchar | 主键,必须唯一不能为空用于唯一标识每一个客户端(client) 在注册时必须填写(也可由服务端自动生成).对于不同的grant type,该字段都是必须的.在实际应用中的另一个名称叫appKey,与client id是同一个概念 |
resource_ids | varchar | 客户端所能访问的资源id集合,多个资源时用逗号,分隔 |
client_secret | varchar | 授权类型,填写clinet_credentials表示客户端模式 |
scope | varchar | 指定客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号()分隔 |
authorized_grant_types | varchar | 指定客户端支持的grant type,可选值包括authorization code.password.refresh tokenimplicitclient credentials,老支持多人grant type用逗号()分隔 |
web_server_redirect_uri | varchar | 客户端的重定向URI,可为空,当grant type为authorization code或implicit时,在auth的流程中会使用并检查与注册时填写的redirect uri是否一致 |
authorities | varchar | 指定客户端所拥有的Spring Security的权限值,可选,若有多个权限值,用逗号()分隔 |
access_token_validity | int | 设定客户端的access token的有效时间值(单位:秒),可选,若不设定值则使用默认的有效时间值(60* 60* 12,12小时) |
refresh_token_validity | int | 设定客户端的refresh token的有效时间值(单位:秒) |
additional_information | varchar | 这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是ISON格式的数据 |
autoapprove | varchar | 设置用户是否自动Approval操作,默认值为false,可选值包括 true,false,read,write.该字段只适用于grant type="authorization code"的情况,当用户登录成功后,若该值为true或支持的scope值,则会跳过用户Approve的页面,直接授权 |
2.2 搭建 auth-commom
这是一个jar包,引入了oauth2.0 依赖,同时配置启用资源服务,需要使用资源服务保护的项目需要依赖这个jar包
auth-commom源码地址
2.2.2 搭建auth-server 授权服务
auth-server 既是授权服务同时也是资源服务,所有引入上面auth-common
auth-server源码地址
三.授权码模式自定义登录页面和授权登录页面
3.1 自定义登录页面和授权页面
3.1.1 登录页面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<style>
.login-container {
margin: 50px;
width: 100%;
}
.form-container {
margin: 0px auto;
width: 50%;
text-align: center;
box-shadow: 1px 1px 10px #888888;
height: 300px;
padding: 5px;
}
input {
margin-top: 10px;
width: 350px;
height: 30px;
border-radius: 3px;
border: 1px #E9686B solid;
padding-left: 2px;
}
.btn {
width: 350px;
height: 35px;
line-height: 35px;
cursor: pointer;
margin-top: 20px;
border-radius: 3px;
background-color: #E9686B;
color: white;
border: none;
font-size: 15px;
}
.title{
margin-top: 5px;
font-size: 18px;
color: #E9686B;
}
</style>
<body>
<div class="login-container">
<div class="form-container">
<p class="title">用户登录</p>
<form name="loginForm" method="post" th:action="${loginProcessUrl}">
<input type="text" name="username" placeholder="用户名"/>
<br>
<input type="password" name="password" placeholder="密码"/>
<br>
<button type="submit" class="btn">登 录</button>
</form>
<p style="color: red" th:if="${param.error}">用户名或密码错误</p>
</div>
</div>
</body>
</html>
3.1.2 授权页面
授权页面要注意form表单的提交地址/auth/oauth/authorize,/auth是网关前缀,/oauth/authorize这个地址在AuthorizationEndpoint中
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>授权</title>
</head>
<style>
html{
padding: 0px;
margin: 0px;
}
.title {
background-color: #E9686B;
height: 50px;
padding-left: 20%;
padding-right: 20%;
color: white;
line-height: 50px;
font-size: 18px;
}
.title-left{
float: right;
}
.title-right{
float: left;
}
.title-left a{
color: white;
}
.container{
clear: both;
text-align: center;
}
.btn {
width: 350px;
height: 35px;
line-height: 35px;
cursor: pointer;
margin-top: 20px;
border-radius: 3px;
background-color: #E9686B;
color: white;
border: none;
font-size: 15px;
}
</style>
<body style="margin: 0px">
<div class="title">
<div class="title-right">OAUTH-BOOT 授权</div>
<div class="title-left">
<a href="#help">帮助</a>
</div>
</div>
<div class="container">
<h3 th:text="${clientId}+' 请求授权,该应用将获取你的以下信息'"></h3>
<p>昵称,头像和性别</p>
授权后表明你已同意 <a href="#boot" style="color: #E9686B">OAUTH-BOOT 服务协议</a>
<form method="post" action="/auth/oauth/authorize">
<input type="hidden" name="user_oauth_approval" value="true">
<input type="hidden" name="_csrf" th:value="${_csrf.getToken()}"/>
<div th:each="item:${scopes}">
<input type="radio" th:name="'scope.'+${item}" value="true" hidden="hidden" checked="checked"/>
</div>
<button class="btn" type="submit"> 同意/授权</button>
</form>
</div>
</body>
</html>
3.2 定义跳转登录页面接口
通过这个auth/login跳转到自定义的登录页面
@Controller
public class AuthLoginController {
@GetMapping("/auth/login")
public String loginPage(Model model){
model.addAttribute("loginProcessUrl","/auth/authorize");
return "base-login";
}
}
3.3 配置WebSecurityConfig
@Order(-1)
@Slf4j
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//配置过滤的url
.requestMatchers()
.antMatchers("/auth/**","/oauth/**")
.and()
//配置过滤的url的拦截策略
.authorizeRequests()
//配置不做校验的url
.antMatchers("/auth/login", "/auth/authorize")
.permitAll()
//除上面指定url外其他地址都需要认证
.anyRequest()
.authenticated();
http.formLogin()
// 登录页面,登录页面也必须接口跳转,因为如果不走接口,oauth不会拦截校验
.loginPage("/auth/login")
// 登录处理url,这个地址并没有真的mapping,是一个假地址目的时登陆后oauth会过滤这个地址跳转到授权页面
.loginProcessingUrl("/auth/authorize");
http.httpBasic().disable();
}
//省略部分代码
}
3.3 配置/custom/confirm_access接口
@Controller
@SessionAttributes("authorizationRequest")
public class CustomizeApprovalEndpoint {
/**
* 自定义授权页面
* @param model
* @param request
* @return
* @throws Exception
*/
@RequestMapping("/custom/confirm_access")
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");
ModelAndView view = new ModelAndView();
view.setViewName("base-grant");
view.addObject("clientId", authorizationRequest.getClientId());
return view;
}
}
3.4 使用/custom/confirm_access接口替换/oauth/confirm_access接口
替换默认的授权页面,跳转到我们自定义的授权页面
@Slf4j
@Configuration
@EnableAuthorizationServer
public class OauthAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
//省略部分代码
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//省略部分代码
endpoints.pathMapping("/oauth/confirm_access","/custom/confirm_access");
}
}
3.5效果展示
请求地址
http://192.168.31.234:8070/auth/oauth/authorize?response_type=code&client_id=order-server&redirect_uri=http://www.baidu.com&scope=read
登录页面
授权页面
返回code
code获取token
四.自定义授权模式-手机号验证码登录
实现自定义手机号+验证码授权模式三要素
1.继承AbstractAuthenticationToken类,对授权账号密码和授权结果进行包装
2.实现AuthenticationProvider的authenticate方法,这里做认证校验
3.继承AbstractTokenGranter类重写getOAuth2Authentication方法,调用authenticationManager的授权方法
4.1 SmsVerificationCodeAuthenticationToken
public class SmsVerificationCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
/**
* 需要认证
* @param principal
* @param credentials
*/
public SmsVerificationCodeAuthenticationToken(Object principal, Object credentials) {
super((Collection) null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
/**
* 不需要认证
* @param principal
* @param credentials
* @param authorities
*/
public SmsVerificationCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
4.2 SmsVerificationCodeAuthenticationProvider
public class SmsVerificationCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public SmsVerificationCodeAuthenticationProvider(UserDetailsService userDetailsService){
this.userDetailsService = userDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsVerificationCodeAuthenticationToken authenticationToken = (SmsVerificationCodeAuthenticationToken) authentication;
String mobileNumber = (String) authentication.getPrincipal();
String verificationCode = (String) authentication.getCredentials();
if (!SmsUtils.verificationCodeIsOk(mobileNumber,verificationCode)){
throw new BusinessException("认证失败,验证码不正确!");
}
//账号信息验证
UserDetails userDetails = ((CustomerUserDetailService) userDetailsService).loadUserByUsername(mobileNumber);
SmsVerificationCodeAuthenticationToken result = new SmsVerificationCodeAuthenticationToken(userDetails, authentication.getCredentials(), new HashSet<>());
result.setDetails(authentication.getDetails());
return result;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsVerificationCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
4.3 SmsVerificationCodeTokenGranter
public class SmsVerificationCodeTokenGranter extends AbstractTokenGranter {
private AuthenticationManager authenticationManager;
public SmsVerificationCodeTokenGranter(AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory,
AuthenticationManager authenticationManager) {
super(tokenServices, clientDetailsService, requestFactory, GrantTypeConstants.MOBILE_CODE);
this.authenticationManager = authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
String mobile = parameters.get("mobile");
String code = parameters.get("code");
parameters.remove("code");
Authentication userAuth = new SmsVerificationCodeAuthenticationToken(mobile, code);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = this.authenticationManager.authenticate(userAuth);
} catch (AccountStatusException var8) {
throw new InvalidGrantException(var8.getMessage());
} catch (BadCredentialsException var9) {
throw new InvalidGrantException(var9.getMessage());
}
if (userAuth != null && userAuth.isAuthenticated()) {
OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
} else {
throw new InvalidGrantException("Could not authenticate user: " + mobile);
}
}
}