Spring Boot 的 oAuth2 认证(附源码)

 

OAuth2 统一认证

原理

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

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

A、用户打开客户端之后,客户端要求用户给予受权。java

B、用户赞成给予客户端受权。mysql

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

D、认证服务器对客户端进行认证之后,确认无误,赞成发放令牌。github

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

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

 

客户端受权模式

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

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

受权码模式

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

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"
     }

 

密码模式

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

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;
    }
}

 

相关问题

问题1、提供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

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

认证后,访问资源服务

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!'); 
  }});

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

从 github上申请 开发权限:

执行流程

1> 点击页面的 callback超连接(好比 <a href="http://localhost:8090/login/github">go to github</a>)。

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

 

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

问题3、 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/
相关文章
相关标签/搜索