Spring Boot 的 oAuth2 认证(附源码)

Stella981
• 阅读 747

OAuth2 统一认证

原理

OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。"客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。

"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。

Spring Boot 的 oAuth2 认证(附源码)

A、用户打开客户端以后,客户端要求用户给予授权。

B、用户同意给予客户端授权。

C、客户端使用上一步获得的授权,向认证服务器申请令牌。

D、认证服务器对客户端进行认证以后,确认无误,同意发放令牌。

E、客户端使用令牌,向资源服务器申请获取资源。

F、资源服务器确认令牌无误,同意向客户端开放资源。

客户端授权模式

上面 B、流程中是用户给予客户端授权。oauth2 定义了下面四种授权方式:

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

授权码模式

Spring Boot 的 oAuth2 认证(附源码)

A、用户访问客户端,后者将前者导向认证服务器。

B、用户选择是否给予客户端授权。

C、假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。

D、客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。

E、认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

步骤说明:

① A步骤中,客户端申请认证的URI,包含以下参数:

* response_type:表示授权类型,必选项,此处的值固定为"code"

* client_id:表示客户端的ID,必选项

* redirect_uri:表示重定向URI,可选项

* scope:表示申请的权限范围,可选项

* state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
        &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1Host: server.example.com

② C步骤中,服务器回应客户端的URI,包含以下参数:

* code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。

* state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。

HTTP/1.1 302 FoundLocation: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
          &state=xyz

③ D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:

* grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。

* code:表示上一步获得的授权码,必选项。

* redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。

* client_id:表示客户端ID,必选项。

POST /token HTTP/1.1Host: server.example.comAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JWContent-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

④ E步骤中,认证服务器发送的HTTP回复,包含以下参数:

* access_token:表示访问令牌,必选项。

* token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。

* expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。

* refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。

* scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。

HTTP/1.1 200 OK
     Content-Type: application/json;charset=UTF-8
     Cache-Control: no-store
     Pragma: no-cache

     {
       "access_token":"2YotnFZFEjr1zCsicMWpAA",
       "token_type":"example",
       "expires_in":3600,
       "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
       "example_parameter":"example_value"
     }

密码模式

密码模式是将授权码模式中的授权码固定为用户名和密码。

Spring Boot 的 oAuth2 认证(附源码)

A、用户向客户端提供用户名和密码。

B、客户端将用户名和密码发给认证服务器,向后者请求令牌。

C、认证服务器确认无误后,向客户端提供访问令牌。

步骤说明:

① B步骤中,客户端发出的HTTP请求,包含以下参数:

* grant_type:表示授权类型,此处的值固定为"password",必选项。

* username:表示用户名,必选项。

* password:表示用户的密码,必选项。

* scope:表示权限范围,可选项。

POST /token HTTP/1.1
     Host: server.example.com
     Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
     Content-Type: application/x-www-form-urlencoded

     grant_type=password&username=johndoe&password=A3ddj3w

② C步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。

HTTP/1.1 200 OK
     Content-Type: application/json;charset=UTF-8
     Cache-Control: no-store
     Pragma: no-cache

     {
       "access_token":"2YotnFZFEjr1zCsicMWpAA",
       "token_type":"example",
       "expires_in":3600,
       "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
       "example_parameter":"example_value"
     }

注意,整个过程中,客户端不得保存用户的密码。

项目实践

1 .pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>

2 .SecurityConfig.java(主要配置文件)

package club.lemos.sso.config;

import club.lemos.sso.config.security.ClientResources;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.filter.CompositeFilter;

import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.List;

@Configuration
@EnableOAuth2Client
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private OAuth2ClientContext oauth2ClientContext;

    private final UserDetailsService userDetailService;

    @Autowired
    public SecurityConfig(UserDetailsService userDetailService) {
        this.userDetailService = userDetailService;
    }

    /**
     * 详细的路由配置参数
     *
     * @param http 配置
     * @throws Exception 相关异常
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors() // 跨域支持
                .and()
                .antMatcher("/**") // 捕捉所有路由
                .authorizeRequests()
                .antMatchers("/", "/login**", "/webjars/**", "/github").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) // 认证入口(跳转)
                .and()
                .formLogin().loginProcessingUrl("/doLogin") // 表单请求的路由为 "POST /login"
                .defaultSuccessUrl("/").failureUrl("/login?err=1")
                .permitAll()
                .and()
                .logout().logoutUrl("/logout") // 注销请求的路由为 "GET /logout"
                .logoutSuccessUrl("/")
                .permitAll()
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .and()
                .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // csrf 安全处理
                .and()
                .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class); // 第三方授权层

    }

    @Bean
    public FilterRegistrationBean oauth2ClientFilterRegistration(
            OAuth2ClientContextFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(filter);
        registration.setOrder(-100);
        return registration;
    }

    private Filter ssoFilter() {
        CompositeFilter filter = new CompositeFilter();
        List<Filter> filters = new ArrayList<>();
        filters.add(ssoFilter(github(), "/login/github"));
        filter.setFilters(filters);
        return filter;
    }

    private Filter ssoFilter(ClientResources client, String path) {
        OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
        OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
        filter.setRestTemplate(template);
        UserInfoTokenServices tokenServices = new UserInfoTokenServices(
                client.getResource().getUserInfoUri(), client.getClient().getClientId());
        tokenServices.setRestTemplate(template);
        filter.setTokenServices(tokenServices);
        return filter;
    }

    /**
     * github 授权连接
     *
     * @return 第三方授权连接对象
     */
    @Bean
    @ConfigurationProperties("github")
    public ClientResources github() {
        return new ClientResources();
    }

    /**
     * BCrypt 密码加密
     *
     * @return BCrypt 编码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DaoAuthenticationProvider authProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailService);
//        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    /**
     * 用户认证服务(用户名+密码)
     *
     * @param auth 认证
     * @throws Exception 相关异常
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authProvider());
    }

//    TODO 提供一个访问用户信息(昵称,角色信息等等)的 api
//    TODO 提供 cookie持久化时间
//    TODO 注销功能实现
}

ClientResources.java

package club.lemos.sso.config.security;

import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;

public class ClientResources {

    @NestedConfigurationProperty
    private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();

    @NestedConfigurationProperty
    private ResourceServerProperties resource = new ResourceServerProperties();

    public AuthorizationCodeResourceDetails getClient() {
        return client;
    }

    public ResourceServerProperties getResource() {
        return resource;
    }
}

3 .application.yml(配置文件)

logging:
    level:
        org:
            springframework:
                security: DEBUG
        root: INFO
server:
    port: 8080
spring:
    datasource:
        dbcp2:
            initial-size: 10
            max-idle: 8
            min-idle: 8
        driverClassName: com.mysql.jdbc.Driver
        password: root
        url: jdbc:mysql://localhost:3306/sso?useSSL=false
        username: root
    freemarker:
        charset: UTF-8
        check-template-location: true
        content-type: text/html
        expose-request-attributes: true
        expose-session-attributes: true
        request-context-attribute: request
    thymeleaf:
        cache: false
        prefix: classpath:/templates/
        suffix: .html
github:
  client:
    clientId: dd2bf79a9e6be256f0e8
    clientSecret: 0e555a2ee5d627e3abdee3f5096de6d8278d3413
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user

4 .UserDetailsServiceImpl.java(用户接口实现)

package club.lemos.sso.config.security;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Date;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // TODO 从数据库中查找用户
        return new UserDetailsImpl(1L, "lisi", "password",
                Arrays.asList(new SimpleGrantedAuthority("USER"), new SimpleGrantedAuthority("ADMIN")), true, new Date());
    }

}

UserDetailsImpl.java

package club.lemos.sso.config.security;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Date;

public class UserDetailsImpl implements UserDetails {
    private final Long id;
    private final String username;
    private final String password;
    private final Collection<? extends GrantedAuthority> authorities;
    private final boolean enabled;
    private final Date lastPasswordResetDate;

    public UserDetailsImpl(Long id, String username, String password, Collection<? extends GrantedAuthority> authorities, boolean enabled, Date lastPasswordResetDate) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.authorities = authorities;
        this.enabled = enabled;
        this.lastPasswordResetDate = lastPasswordResetDate;
    }

    @JsonIgnore
    public Long getId() {
        return id;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    @JsonIgnore
    public Date getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }
}

相关问题

问题一、提供oAuth2 接口 及 token 的获取

之前的Web端(B\S结构),可以正常通信。登录跳转什么的。对于受限资源,需要通过 oauth2授权( C\S 结构)。可以通过Web端发送一个请求进行认证。认证成功,获得 token,可以使用 token访问f服务器受限资源。formLogin 即表单登录,比较好理解。oauth2 登录,需要先获得 token,再用它访问 api 资源服务器,获取信息。

1. 证书 token

curl client:secret@localhost:8090/oauth/token -d grant_type=client_credentials

Spring Boot 的 oAuth2 认证(附源码)

2. 密码 token

当应用启动时,springboot 会创建一个默认的用户,用户id为‘user‘。密码是随机的,但可以从打印的日志中看到。

curl client:secret@localhost:8090/oauth/token -d grant_type=password -d username=user -d password=...

或者使用定义好的用户名及密码进行认证

curl client:secret@localhost:8090/oauth/token -d grant_type=password -d username=admin -d password=admin

Spring Boot 的 oAuth2 认证(附源码)

认证后,访问资源服务

curl http://localhost:8090/api/users -H "Authorization: bearer 7e7b7ced-3747-43a2-8134-c7e6b87c6451"

3. 页面中,可以通过发送带 auth 认证头的请求,访问oauth服务器

ajax 请求认证:

$.ajax({
  type: "GET",
  url: "index1.php",
  dataType: 'json',
  async: false,
  headers: {
    "Authorization": "Basic " + btoa(USERNAME + ":" + PASSWORD)
  },
  data: '{ "comment" }',
  success: function (){
    alert('Thanks for your comment!'); 
  }});

Spring Boot 的 oAuth2 认证(附源码)

问题二、github 第三方授权接入(原文:https://developer.github.com/v3/oauth/

从 github上申请 开发权限:

Spring Boot 的 oAuth2 认证(附源码)

执行流程

1> 点击页面的 callback超链接(比如 go to github)。

callbackURL = http://localhost:8090/login/github  GET

2> 重定向到授权页面,用户点击授权

user-authorization-uri = https://github.com/login/oauth/authorize?client_id=141c0a61de83cf2d9841&redirect_uri=http://localhost:8090/login/github&response_type=code&scope=user&state=4jgVT2

Spring Boot 的 oAuth2 认证(附源码)

3>  重定向回自己的页面,并携带一个 code (授权码) 和 前一步中的 state参数,如果 states 匹配,则可以发送一个 POST https://github.com/login/oauth/access_token

http://localhost:8090/login/github?code=2c6dcdce82ef1473e148&state=4jgVT2

4> 请求 token(授权成功,会自动发送这个请求)

https://github.com/login/oauth/access_token    POST 

响应 token(包含着授权信息,存储在 JSESSION 中)

access_token=e72e16c7e42f292c6912e7710c838347ae178b4a&scope=user%2Cgist&token_type=bearer

或者

Accept: application/json {"access_token":"e72e16c7e42f292c6912e7710c838347ae178b4a", "scope":"repo,gist", "token_type":"bearer"}

5> 访问 Github API(使用 js访问)

GET https://api.github.com/user?access_token=...

或者设置头信息

Authorization: token OAUTH-TOKEN

问题三、 github 的token的获取

使用 RestTemplate访问。

OAuth2RestTemplate template = oAuth2RestTemplate(new AuthorizationCodeResourceDetails());
template.setRetryBadAccessTokens(false);
token = template.getAccessToken();

或者在配置文件中从上下文中直接获取。

OAuth2AccessToken accessToken = oauth2ClientContext.getAccessToken();

完整项目下载

完整项目下载—— 点我

其他

Jwt(json web tokens)

Spring security 框架实现原理

参阅文档

springboot 官方文档

Spring-Boot-Reference-Guide
https://qbgbook.gitbooks.io/spring-boot-reference-guide-zh/content/

Spring Boot and OAuth2 *****接入github 的详细配置******
https://spring.io/guides/tutorials/spring-boot-oauth2/

---------------------------------------------------------------------------

有用的文章

spring security & oauth2
http://www.jianshu.com/p/6b211e845b16/

spring-security-oauth2 server
http://www.jianshu.com/p/028043425b09

详解Spring Security进阶身份认证之UserDetailsService(附源码)
http://favccxx.blog.51cto.com/2890523/1609692

---------------------------------------------------------------------------

github 相关文档

https://developer.github.com/v3/

https://developer.github.com/v3/oauth/

https://developer.github.com/v3/oauth_authorizations/#list-your-authorizations

https://help.github.com/articles/connecting-with-third-party-applications/
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
Spring Cloud下基于OAUTH2认证授权的实现
SpringCloud下基于OAUTH2认证授权的实现博客分类:javaspring在SpringCloud需要使用OAUTH2来实现多个微服务的统一认证授权,通过向OAUTH服务发送某个类型的granttype进行集中认证和授权,从而获得access_token,而这个token是受其他微服务信任的,我们在后续的访问可以
Easter79 Easter79
2年前
SpringSecurity进阶:OAuth2.0详解
OAuth2是什么?OAuth是一个为了方便用户登入而使用的授权流程,他的优点是不需要向第三方平台暴露我们的用户名和密码,而是使用授权服务器颁发短期的token和效验token的方式开放部分资源给第三方平台OAuth是一个授权协议不是认证协议OAuth2的授权方式!(https://p1tt
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这