oauth2.0统一授权

统一授权服务

  • 1.oauth2.0授权模式介绍
  • 2.授权服务搭建,资源服务配置
  • 3.授权码模式自定义登陆页面和授权页面
  • 4.自定义手机号+验证码授权模式

一.oauth2.0介绍

OAuth2.0提供了四种授权(获取令牌)方式,四种方式均采用不同的执行流程,让我们适应不同的场景,除此之外我们还可以通过TokenGranter扩展自定义授权模式
OAuth2.0中有四个重要角色:

Oauth2.0 角色 说明
客户端 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源
资源拥有者 通常为用户,也可以是应用程序,资源的拥有者
授权服务 用户服务提供商对资源拥有的身份认证,对访问资源进行授权,认证成功后发放令牌,作为客户端访问资源的凭据
资源服务 存储资源服务器

1.1.授权模式

1.1.1 授权码模式 authorization_code

这种模式是四种模式中最安全的一种模式。一般用于Web服务器端应用或第三方的原生App调用资源服务的时候。

image.png

客户端需要访问资源服务需要以下七个步骤
(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


image.png

(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 .简化模式

image.png

(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 .客户端模式

image.png

(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">登 &nbsp;&nbsp; 录</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

登录页面

image.png

授权页面
image.png

返回code

image.png

code获取token

image.png

四.自定义授权模式-手机号验证码登录

实现自定义手机号+验证码授权模式三要素
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);
        }
    }
}

5.接入微信登陆

6.新的授权框架spring-authorization-server

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容