SpringBoot整合OAuth 2.0

OAuth 2.0为OAuth的协议的延续版本,是一个关于授权的开放网络标准,在全世界得到广泛应用。基于OAuth 2.0授权有一下几个特点:1、安全:平台商无需使用用户的用户名与密码就可以申请获得该用户资源的授权;2、开放:任何消费方都可以使用 OAuth 认证服务,任何服务提供方 ( Service Provider) 都可以实现自身的 OAuth 认证服务。3、简单:不管是消费方还是服务提供方,都很容易于理解与使用。OAuth 2.0认证流程如下:

provider为服务提供方,即第三方服务系统,主要用作管理用户资源和为其他系统提供认证服务。例如:我们使用的QQ账号是在腾讯的服务器上,腾讯提供统一对外接口给各个平台(如:今日头条),用户下载今日头条app后,就可以选择使用QQ登录今日头条app。用户的个人信息是存放于腾讯服务器上的,用户可以授权今日头条获取自己在腾讯服务器上的个人信息,今日头条获取到用户信息后存于自己的系统用,以继续为用户服务。一般用户信息的存储和用户登录认证都是在provider方完成。

client为平台系统,如:各个科技公司开发的系统,平台系统为用户提供更为具体的服务,比如:看新闻、看视频、玩游戏等等。client想要使用provider提供的第三方登录服务,必须要先向provider注册自己,也就是需要让provider识别自己是不是可信的平台服务商,所以在注册时provider会下发给各个client一组账号密码来唯一标识各个平台,从而可配置provider开放给各个client的访问域。

user即普通用户,普通用户使用平台商提供的服务。

传统的平台系统都各自有一套自己的用户认证体系,这就导致用户每使用一个平台都需要注册一组账号密码,时间久了就记不清了,从而导致用户的流失。使用OAuth 2.0提供的统一认证服务会很好的解决这个问题,用户只要记住一组账号密码,就可以在很多平台登录,提升了用户体验。

1、provider整合注意事项

在oauth2-provider模块中,存放着用户的详细信息,一般包括用户名、密码、昵称、手机号、地址等等信息。需要在MyUserDetailsService的UserService实现自定义的用户认证方法,即:自定义实现用户表user,查询用户数据需要自己实现,同时实现MyUserDetails配置用户的账号密码、权限、是否过期、是否禁用等等,以便OAuth 2.0框架识别用户的一些基本信息以达到登录认证的作用。MyClientDetailsService的ClientService实现平台商的认证方法,对应到数据库的client表,存放平台商的一些基本账户信息和授权码回调地址,以及token过期时间。MyClientDetails配置了平台商的访问域、认证方式(主要有:authorization_code、password、client_credentials、implicit、refresh_ token五种,本文只介绍authorization_code方式的认证)、token过期等,系统根据你的配置就会限制平台商的一些访问操作及token过期等。

SecurityConfig中主要是开启spring-security和配置一些拦截过滤以及启用自定义用户认证,用户使用第三方登录时输入账号密码将在此处配置认证。ResourceServerConfig中主要配置对哪些资源路径进行拦截认证,例如:对获取用户的手机号拦截,对获取用户的昵称不进行拦截。AuthorizationServerConfig主要启用平台商认证配置。

在UserController中实现用户登录并授权成功后,client端可以通过下发的token访问到用户信息。

2、client整合注意事项

client端主要负责以下几个工作:

  1. 为用户重定向到资源服务器进行登录认证。
  2. 认证成功后获取资源服务器授权码的回调。
  3. 根据授权码获取access_token和refresh_token。
  4. 使用access_token从资源服务器获取用户私有信息,并存入自身系统,让用户成为自己系统的用户。
  5. 维护access_token的生命周期,当发现access_token过期时,需要让用户在无感知情况下使用refresh_token来刷新access_token。
  6. 当refresh_token过期后,若用户还想访问自己的私有信息,则需要提示用户重新登录。

client端需要对用户暴露第三方登录入口,如:QQ登录、微信登录、支付宝登录等第三方授权平台。当用户选择第三方登录时,需要根据选择的入口提供不同的认证服务器登录页面的重定向。

当将用户重定向到第三方服务器进行登录并授权后,第三方服务器会根据平台商注册的回调地址将授权码回调给平台商。平台商根据授权码和平台商账号密码向第三方资源服务器获取access_token和refresh_token,使用access_token来获取用户信息,使用refresh_token来刷新access_token。

当平台商获取到用户信息后,会将用户信息保存到自身系统的用户表中,同时提示用户登录成功。第三方返回给平台商的用户信息可能会缺少必要信息,比如:手机号、住址等,第三方一般不会提供如此敏感的信息,这就需要平台商自己提示用户:绑定手机号享XXX服务等来引导用户继续完善自己的用户信息以获得更为精准的服务。

当用户使用第三方登录成功后,可能后续还会获取用户的其他资源,如:用户想在平台商提供的相册功能访问自己在第三方的相册,那么平台商就会继续使用access_token访问第三方服务器来获取用户的相册。此时可能access_token已经过期,那么client就需要在用户无感知的情况下使用refresh_token来换取新的access_token继续访问用户资源。众所周知,token都有过期时间,当refresh_token也过期的话,就需要告诉用户,登录已经过期,想要继续访问私有资源必要要重新登录,那么再进行登录流程获取用户信息和新的token。

整合开始:

项目采用多模块形式:

oauth2-parent.pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.xujianjie</groupId>
    <artifactId>oauth2-parent</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <name>parent</name>

    <modules>
        <module>oauth2-common</module>
        <module>oauth2-client</module>
        <module>oauth2-provider</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencyManagement>
        <dependencies>

            <!--spring-security-oauth2-->
            <dependency>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                <version>2.0.0.RELEASE</version>
            </dependency>

            <!--mysql-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.25</version>
            </dependency>

            <!--mybatis-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.0.0</version>
            </dependency>

            <!--fastJson-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.54</version>
            </dependency>

            <!--org-json-->
            <dependency>
                <groupId>org.json</groupId>
                <artifactId>json</artifactId>
                <version>20180813</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

</project>

oauth2-provider.pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.xujianjie</groupId>
        <artifactId>oauth2-parent</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>oauth2.provider</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>oauth2-provider</name>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

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

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.20.1</version>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

 oauth2-client.pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.xujianjie</groupId>
        <artifactId>oauth2-parent</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>oauth2.client</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>oauth2-client</name>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.xujianjie</groupId>
            <artifactId>oauth2.common</artifactId>
            <version>1.0.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.20.1</version>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

oauth2-common.pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.xujianjie</groupId>
        <artifactId>oauth2-parent</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>oauth2.common</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>oauth2-common</name>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>

</project>

oauth2-provider模块:

SecurityConfig.java

package com.xujianjie.oauth2.provider.config;

import com.xujianjie.oauth2.provider.config.auth.service.MyUserDetailsService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    @Autowired
    private MyUserDetailsService myUserDetailsService;

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

    //service用户认证
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception
    {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(myUserDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        builder.authenticationProvider(provider);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http.formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                .and()
                .authorizeRequests()
                .antMatchers("/login.html", "/img/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
    }
}

ResourceServerConfig.java

package com.xujianjie.oauth2.provider.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

//资源服务器配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter
{
    @Override
    public void configure(ResourceServerSecurityConfigurer resources)
    {
        resources.resourceId("resource-info");
    }

    @Override
    public void configure(HttpSecurity http) throws Exception
    {
        //配置拦截接口
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .requestMatchers()
                .antMatchers("/api/user/**")
                .and().authorizeRequests()
                .antMatchers("/api/user/**")
                .authenticated();
    }
}

AuthorizationServerConfig.java

package com.xujianjie.oauth2.provider.config;

import com.xujianjie.oauth2.provider.config.auth.service.MyClientDetailsService;
import com.xujianjie.oauth2.provider.config.auth.service.MyUserDetailsService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;

//授权服务器配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter
{
    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Autowired
    private MyClientDetailsService myClientDetailsService;

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    //service企业认证
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception
    {
        clients.withClientDetails(myClientDetailsService);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
    {
        endpoints.authenticationManager(authenticationManager).userDetailsService(myUserDetailsService);
    }
}

MyUserDetailsService.java

package com.xujianjie.oauth2.provider.config.auth.service;

import com.xujianjie.oauth2.provider.config.auth.model.MyUserDetails;
import com.xujianjie.oauth2.provider.model.User;
import com.xujianjie.oauth2.provider.service.UserService;

import org.springframework.beans.factory.annotation.Autowired;
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;

@Service
public class MyUserDetailsService implements UserDetailsService
{
    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username)
    {
        User user = userService.findByAccount(username);
        if (user == null)
        {
            throw new UsernameNotFoundException(username);
        }

        return new MyUserDetails(user);
    }
}

MyUserDetails.java

package com.xujianjie.oauth2.provider.config.auth.model;

import com.xujianjie.oauth2.provider.model.User;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

public class MyUserDetails implements UserDetails
{
    private User user;

    public MyUserDetails(User user)
    {
        this.user = user;
    }

    //权限列表
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
        return new ArrayList<>();
    }

    @Override
    public String getPassword()
    {
        return user.getPassword();
    }

    @Override
    public String getUsername()
    {
        return user.getAccount();
    }

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

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

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

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

MyClientDetailsService.java

package com.xujianjie.oauth2.provider.config.auth.service;

import com.xujianjie.oauth2.provider.config.auth.model.MyClientDetails;
import com.xujianjie.oauth2.provider.model.Client;
import com.xujianjie.oauth2.provider.service.ClientService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.stereotype.Service;

@Service
public class MyClientDetailsService implements ClientDetailsService
{
    @Autowired
    private ClientService clientService;

    @Override
    public ClientDetails loadClientByClientId(String account) throws ClientRegistrationException
    {
        Client client = clientService.findByAccount(account);
        if (client == null)
        {
            throw new ClientRegistrationException("企业客户未注册!");
        }

        return new MyClientDetails(client);
    }
}

 MyClientDetails.java

package com.xujianjie.oauth2.provider.config.auth.model;

import com.xujianjie.oauth2.provider.model.Client;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class MyClientDetails implements ClientDetails
{
    private Client client;

    public MyClientDetails(Client client)
    {
        this.client = client;
    }

    @Override
    public String getClientId()
    {
        return client.getAccount();
    }

    @Override
    public Set<String> getResourceIds()
    {
        return new HashSet<>();
    }

    @Override
    public boolean isSecretRequired()
    {
        return true;
    }

    @Override
    public String getClientSecret()
    {
        return new BCryptPasswordEncoder().encode(client.getPassword());
    }

    @Override
    public boolean isScoped()
    {
        return true;
    }

    @Override
    public Set<String> getScope()
    {
        Set<String> set = new HashSet<>();
        set.add("read");

        return set;
    }

    @Override
    public Set<String> getAuthorizedGrantTypes()
    {
        Set<String> set = new HashSet<>();
        set.add("authorization_code");
        set.add("refresh_token");

        return set;
    }

    @Override
    public Set<String> getRegisteredRedirectUri()
    {
        Set<String> set = new HashSet<>();
        set.add(client.getCallBackUrl());

        return set;
    }

    @Override
    public Collection<GrantedAuthority> getAuthorities()
    {
        return new HashSet<>();
    }

    @Override
    public Integer getAccessTokenValiditySeconds()
    {
        return client.getAccessTokenOverdueSeconds();
    }

    @Override
    public Integer getRefreshTokenValiditySeconds()
    {
        return client.getRefreshTokenOverdueSeconds();
    }

    @Override
    public boolean isAutoApprove(String s)
    {
        return true;
    }

    @Override
    public Map<String, Object> getAdditionalInformation()
    {
        return new HashMap<>();
    }
}

UserController.java

package com.xujianjie.oauth2.provider.controller;

import com.xujianjie.oauth2.provider.model.User;
import com.xujianjie.oauth2.provider.service.UserService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
@RequestMapping("/api/user")
public class UserController
{
    @Autowired
    private UserService userService;

    @RequestMapping(value = "/info", method = RequestMethod.GET)
    public User info(Principal principal)
    {
        User user = userService.findByAccount(principal.getName());
        user.setPassword("");

        return user;
    }
}

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>欢迎登录</title>
    <style type="text/css">
        body, h1, h2, h3, h4, h5, h6, hr, p, blockquote,
        dl, dt, dd, ul, ol, li,
        pre,
        form, fieldset, legend, button, input, textarea,
        th, td {
            margin: 0;
            padding: 0;
        }

        .bg {
            display: flex;
            justify-content: center;
            align-items: center;
            width: 100vw;
            height: 100vh;
            background: url("./img/bg_login.png") no-repeat;
            background-size: 100% 100%;
        }

        .input {
            width: 300px;
            height: 35px;
            padding: 0 10px 0 10px;
            color: #333333;
            font-size: 14px;
            border: 0;
            border-radius: 3px;
        }

        .login-button {
            width: 320px;
            height: 40px;
            background-color: #65a5ff;
            border: 0;
            border-radius: 3px;
            margin-top: 30px;
            color: #ffffff;
            font-size: 14px;
        }
    </style>
</head>
<body>
<div class="bg">
    <div style="display: flex; flex-direction: column; align-items: center;">
        <img src="img/icon_oauth2.png" width="124">
        <form style="margin-top: 40px;" method="post" action="/login">
            <p>
                <label for="username"></label>
                <input class="input" type="text" id="username" name="username" placeholder="请输入用户名" required>
            </p>
            <p style="margin-top: 15px;">
                <label for="password"></label>
                <input class="input" type="password" id="password" name="password" placeholder="请输入密码" required>
            </p>
            <button class="login-button" type="submit">登录</button>
        </form>
    </div>
</div>
</body>
</html>

 application.properties

#端口号
server.port=8001

#mySql
spring.datasource.url=jdbc:mysql://localhost:3306/oauth2-provider
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

#myBatis
#sql输出
logging.level.com.xujianjie.orgpermission.mapper=debug
#实体映射遵循驼峰命名
mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.useColumnLabel=true

oauth-client模块:

UserController.java

package com.xujianjie.oauth2.client.controller;

import com.xujianjie.oauth2.client.exception.MyException;
import com.xujianjie.oauth2.client.model.params.UserLoginParams;
import com.xujianjie.oauth2.client.model.po.User;
import com.xujianjie.oauth2.client.service.UserService;
import com.xujianjie.oauth2.common.model.ResponseData;
import com.xujianjie.oauth2.common.utils.ServerUtil;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

import javax.servlet.http.HttpServletResponse;

@RestController
@RequestMapping(value = "/api/user")
public class UserController
{
    @Autowired
    private UserService userService;

    @Value("${third-login-redirect-url}")
    private String thirdLoginRedirectUrl;

    @RequestMapping(value = "/thirdLogin", method = RequestMethod.GET)
    public void thirdLogin(HttpServletResponse response)
    {
        try
        {
            //重定向到资源服务器进行身份验证
            response.sendRedirect(thirdLoginRedirectUrl);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    @RequestMapping(value = "/thirdInfo")
    public ResponseData thirdInfo(Integer userId)
    {
        if (!ServerUtil.validateObjectParamsSuccess(userId))
        {
            throw new MyException(ResponseData.STATUS_REQUEST_FAILED, "用户id不能为空!");
        }

        User user = userService.findThirdUser(userId);
        if (user != null)
        {
            return new ResponseData(ResponseData.STATUS_OK, user, "获取成功!");
        }
        else
        {
            return new ResponseData(ResponseData.STATUS_REQUEST_FAILED, null, "用户不存在!");
        }
    }
}

TokenController.java

package com.xujianjie.oauth2.client.controller;

import com.xujianjie.oauth2.client.exception.MyException;
import com.xujianjie.oauth2.client.model.po.User;
import com.xujianjie.oauth2.client.service.UserService;
import com.xujianjie.oauth2.common.model.ResponseData;

import org.apache.tomcat.util.codec.binary.Base64;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/token")
public class TokenController
{
    @Value("${auth-call-back-url}")
    private String authCallBackUrl;

    @Value("${auth.client-id}")
    private String clientId;

    @Value("${auth.password}")
    private String clientPassword;

    @Value("${third-token-url}")
    private String thirdTokenUrl;

    @Value("${get-third-user-info-url}")
    private String getThirdUserInfoUrl;

    @Autowired
    private UserService userService;

    //第三方资源服务器返回用户认证成功标识authCode
    @RequestMapping(value = "/callBack", method = RequestMethod.GET)
    public ResponseData callBack(String code)
    {
        HttpHeaders headers = new HttpHeaders();
        headers.add("authorization", "Basic " + new String(Base64.encodeBase64((clientId + ":" + clientPassword).getBytes())));

        MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("code", code);
        params.add("client_id", clientId);
        params.add("redirect_uri", authCallBackUrl);

        try
        {
            String result = new RestTemplate().postForObject(thirdTokenUrl, new HttpEntity<>(params, headers), String.class);
            if (result == null)
            {
                throw new MyException(ResponseData.STATUS_REQUEST_FAILED, "企业认证失败!");
            }

            JSONObject json = new JSONObject(result);

            return handleThirdUser(json.optString("access_token"), json.optString("refresh_token"));
        }
        catch (RestClientResponseException e)
        {
            e.printStackTrace();
        }

        return new ResponseData(ResponseData.STATUS_REQUEST_FAILED, "", "登录失败!");
    }

    private ResponseData handleThirdUser(String accessToken, String refreshToken)
    {
        HttpHeaders headers = new HttpHeaders();
        headers.add("authorization", "Bearer " + accessToken);

        try
        {
            String result = new RestTemplate().exchange(getThirdUserInfoUrl, HttpMethod.GET, new HttpEntity<>(headers), String.class).getBody();

            //将第三方用户写入本系统中
            JSONObject json = new JSONObject(result);
            User user = new User();
            user.setAccount("user_" + System.currentTimeMillis());
            user.setPassword("123456");
            user.setNickName(json.optString("nickName"));
            user.setMobile(json.optString("mobile"));
            user.setThirdAccount(json.optString("account"));
            user.setAccessToken(accessToken);
            user.setRefreshToken(refreshToken);

            return new ResponseData(ResponseData.STATUS_OK, userService.addUser(user), "登录成功!");
        }
        catch (RestClientResponseException e)
        {
            e.printStackTrace();
        }

        return new ResponseData(ResponseData.STATUS_REQUEST_FAILED, "", "登录失败!");
    }
}

UserServiceImpl.java

package com.xujianjie.oauth2.client.service.impl;

import com.xujianjie.oauth2.client.exception.MyException;
import com.xujianjie.oauth2.client.mapper.UserMapper;
import com.xujianjie.oauth2.client.model.po.User;
import com.xujianjie.oauth2.client.service.UserService;
import com.xujianjie.oauth2.common.model.ResponseData;

import org.apache.tomcat.util.codec.binary.Base64;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;

@Transactional
@Service
public class UserServiceImpl implements UserService
{
    @Value("${get-third-user-info-url}")
    private String getThirdUserInfoUrl;

    @Value("${third-token-url}")
    private String thirdTokenUrl;

    @Value("${auth.client-id}")
    private String clientId;

    @Value("${auth.password}")
    private String clientPassword;

    @Autowired
    private UserMapper userMapper;

    @Override
    public User addUser(User user)
    {
        String thirdAccount = user.getThirdAccount();
        if (thirdAccount != null && !thirdAccount.equals(""))
        {
            User findUser = userMapper.findByThirdAccount(thirdAccount);
            if (findUser == null)
            {
                if (userMapper.insert(user) == 1)
                {
                    return user;
                }
            }
            else
            {
                user.setUserId(findUser.getUserId());

                return updateUser(user);
            }
        }

        return null;
    }

    @Override
    public User findById(int userId)
    {
        return userMapper.findById(userId);
    }

    @Override
    public User updateUser(User user)
    {
        User updateUser = findById(user.getUserId());
        if (updateUser == null)
        {
            throw new MyException(ResponseData.STATUS_REQUEST_FAILED, "用户不存在!");
        }

        if (user.getPassword() != null)
        {
            updateUser.setPassword(user.getPassword());
        }

        if (user.getNickName() != null)
        {
            updateUser.setNickName(user.getNickName());
        }

        if (user.getMobile() != null)
        {
            updateUser.setMobile(user.getMobile());
        }

        if (user.getAccessToken() != null)
        {
            updateUser.setAccessToken(user.getAccessToken());
        }

        if (user.getRefreshToken() != null)
        {
            updateUser.setRefreshToken(user.getRefreshToken());
        }

        if (userMapper.update(updateUser) == 1)
        {
            return updateUser;
        }
        else
        {
            throw new MyException(ResponseData.STATUS_REQUEST_FAILED, "更新失败!");
        }
    }

    @Override
    public User validateUser(String account, String password)
    {
        User findUser = userMapper.findByAccount(account);
        if (findUser != null && findUser.getPassword().equals(password))
        {
            return findUser;
        }

        return null;
    }

    @Override
    public User findThirdUser(int userId)
    {
        User findUser = findById(userId);

        String accessToken = findUser.getAccessToken();
        if (accessToken != null && !accessToken.equals(""))
        {
            HttpHeaders headers = new HttpHeaders();
            headers.add("authorization", "Bearer " + accessToken);

            try
            {
                String result = new RestTemplate().exchange(getThirdUserInfoUrl, HttpMethod.GET, new HttpEntity<>(headers), String.class).getBody();

                //更新用户信息
                JSONObject json = new JSONObject(result);
                findUser.setNickName(json.optString("nickName"));
                findUser.setMobile(json.optString("mobile"));
                findUser.setThirdAccount(json.optString("account"));
                updateUser(findUser);

                return findUser;
            }
            catch (RestClientResponseException e)
            {
                if (e.getRawStatusCode() == 401)
                {
                    String newAccessToken = refreshAccessToken(findUser.getRefreshToken());
                    if (newAccessToken.equals(""))
                    {
                        //refreshToken过期后清空refreshToken和aceessToken
                        findUser.setAccessToken("");
                        findUser.setRefreshToken("");
                        updateUser(findUser);

                        throw new MyException(ResponseData.STATUS_IDENTITY_OVERDUE, "第三方登录已过期,请重新登录!");
                    }

                    //登录未过期,更新accessToken
                    findUser.setAccessToken(newAccessToken);
                    updateUser(findUser);

                    return findThirdUser(userId);
                }
            }
        }

        return null;
    }

    //以refreshToken刷新accessToken
    private String refreshAccessToken(String refreshToken)
    {
        HttpHeaders headers = new HttpHeaders();
        headers.add("authorization", "Basic " + new String(Base64.encodeBase64((clientId + ":" + clientPassword).getBytes())));

        MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "refresh_token");
        params.add("refresh_token", refreshToken);

        String accessToken;
        try
        {
            String result = new RestTemplate().postForObject(thirdTokenUrl, new HttpEntity<>(params, headers), String.class);
            accessToken = new JSONObject(result).optString("access_token");
        }
        catch (RestClientResponseException e)
        {
            //400 badRequest
            accessToken = "";
        }

        return accessToken;
    }
}

 application.properties

#ip地址
ip-address: localhost

#端口号
server.port=8002

#mySql
spring.datasource.url=jdbc:mysql://localhost:3306/oauth2-client
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

#myBatis
#sql输出
logging.level.com.xujianjie.orgpermission.mapper=debug
#实体映射遵循驼峰命名
mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.useColumnLabel=true

#客户端账号
auth.client-id=yncw
auth.password=123456

#认证重定向地址
third-login-redirect-url=http://localhost:8001/oauth/authorize?response_type=code&client_id=${auth.client-id}&redirect_uri=${auth-call-back-url}

#authCode回调地址
auth-call-back-url=http://${ip-address}:${server.port}/token/callBack

#从资源服务器获取和刷新token地址
third-token-url=http://localhost:8001/oauth/token

#登录成功后获取用户信息
get-third-user-info-url=http://localhost:8001/api/user/info

 MYSQL数据库:

oauth2-provider.sql

/*
Navicat MySQL Data Transfer

Source Server         : localhost
Source Server Version : 50721
Source Host           : localhost:3306
Source Database       : oauth2-provider

Target Server Type    : MYSQL
Target Server Version : 50721
File Encoding         : 65001

Date: 2019-08-20 11:56:47
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for client
-- ----------------------------
DROP TABLE IF EXISTS `client`;
CREATE TABLE `client` (
  `client_id` int(1) NOT NULL AUTO_INCREMENT COMMENT '客户企业id',
  `client_name` varchar(100) DEFAULT NULL COMMENT '企业名称',
  `account` varchar(100) DEFAULT NULL COMMENT '企业账号',
  `password` varchar(100) DEFAULT NULL COMMENT '密码',
  `call_back_url` varchar(255) DEFAULT NULL COMMENT '认证回调地址',
  `access_token_overdue_seconds` int(1) DEFAULT NULL COMMENT 'access_token过期时间(秒)',
  `refresh_token_overdue_seconds` int(1) DEFAULT NULL COMMENT 'refresh_token过期时间(秒)',
  PRIMARY KEY (`client_id`),
  UNIQUE KEY `index_account` (`account`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of client
-- ----------------------------
INSERT INTO `client` VALUES ('1', '云南成为智能科技有限公司', 'yncw', '123456', 'http://localhost:8002/token/callBack', '20', '60');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `user_id` int(1) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `account` varchar(100) DEFAULT NULL COMMENT '用户名',
  `password` varchar(100) DEFAULT NULL COMMENT '密码',
  `nick_name` varchar(100) DEFAULT NULL COMMENT '用户昵称',
  `head_icon` varchar(255) DEFAULT NULL COMMENT '用户头像地址',
  `mobile` varchar(100) DEFAULT NULL COMMENT '手机号',
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `index_account` (`account`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'xujianjie', '$2a$10$.5xnLBrwhj/qjdj/W8tYde.K8ALuC1wbUg8YYFMpx0jjeB7tM3Ctu', '徐建杰', '/head-icon.png', '18887654321');

 oauth2-client.sql

/*
Navicat MySQL Data Transfer

Source Server         : localhost
Source Server Version : 50721
Source Host           : localhost:3306
Source Database       : oauth2-client

Target Server Type    : MYSQL
Target Server Version : 50721
File Encoding         : 65001

Date: 2019-08-20 11:56:39
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `user_id` int(1) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `account` varchar(100) DEFAULT NULL COMMENT '用户名',
  `password` varchar(100) DEFAULT NULL COMMENT '密码',
  `nick_name` varchar(100) DEFAULT NULL COMMENT '用户昵称',
  `mobile` varchar(100) DEFAULT NULL COMMENT '手机号',
  `third_account` varchar(100) DEFAULT NULL COMMENT '第三方用户名',
  `access_token` varchar(255) DEFAULT NULL COMMENT '第三方服务器token',
  `refresh_token` varchar(255) DEFAULT NULL COMMENT '第三方服务器刷新access_token的token',
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `index_account` (`account`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'admin', '123456', '超级管理员', '18888888888', null, null, null);
INSERT INTO `user` VALUES ('4', 'user_1566011157468', '123456', '徐建杰', '18887654321', 'xujianjie', '091d473f-64de-46da-9942-cc61bd094892', '889dc870-bfec-407c-b3bc-4bb8d6e83fe2');

 运行:

打开浏览器,输入地址:http://localhost:8002/api/user/thirdLogin  会自动重定向到8001服务器进行用户认证:

输入用户名密码认证通过后,client端会自动为用户进行token获取与刷新、用户信息获取与存储等操作,所展现给用户的操作就是:选择第三方登录、进行第三方登录、第三方登录成功。client端会处理与provider端的复杂交互。

获取用户信息:

access_token过期后client端会自动刷新token,若是refresh_token也过期的话,会提示用户重新登录,此时需要再次访问http://localhost:8002/api/user/thirdLogin 登录后就可以让client端获取新的token继续访问用户信息。

将近两个星期的OAuth 2.0的研究,基本掌握如何使用这套框架进行用户认证与提供单点登录服务,也搜索了大量文章,但是很多文章没有系统详细的说明,所以今天把它记录在这,希望能帮助更多想使用OAuth 2.0的开发者!

github源代码

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
Spring Boot可以与OAuth 2.0协议进行整合,实现授权和认证功能。下面是一些步骤: 1. 配置pom.xml文件,添加Spring Security OAuth2依赖。 ``` <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.7.RELEASE</version> </dependency> ``` 2. 配置application.properties文件,添加OAuth2配置。 ``` # OAuth2 configuration security.oauth2.client.client-id=client-id security.oauth2.client.client-secret=client-secret security.oauth2.client.access-token-uri=https://example.com/oauth/token security.oauth2.client.user-authorization-uri=https://example.com/oauth/authorize security.oauth2.client.scope=read,write security.oauth2.client.grant-type=authorization_code security.oauth2.resource.token-info-uri=https://example.com/oauth/check_token security.oauth2.resource.user-info-uri=https://example.com/userinfo ``` 3. 创建一个OAuth2客户端,用于向授权服务器发送请求并获取访问令牌。 ``` @Configuration @EnableOAuth2Sso public class OAuth2ClientConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/login**", "/error**") .permitAll() .anyRequest() .authenticated() .and() .logout() .logoutSuccessUrl("/") .permitAll() .and() .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); } @Bean public OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) { return new OAuth2RestTemplate(resource, context); } @Bean @ConfigurationProperties("security.oauth2.client") public OAuth2ProtectedResourceDetails oauth2RemoteResource() { return new AuthorizationCodeResourceDetails(); } @Bean public FilterRegistrationBean oauth2ClientFilterRegistration( OAuth2ClientContextFilter filter) { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(filter); registration.setOrder(-100); return registration; } } ``` 4. 创建一个OAuth2资源服务器,用于保护受保护的资源。 ``` @Configuration @EnableResourceServer public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/api/**") .authenticated(); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("my-resource-id"); } } ``` 5. 创建一个授权服务器,用于颁发访问令牌。 ``` @Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private BCryptPasswordEncoder passwordEncoder; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client-id") .secret(passwordEncoder.encode("client-secret")) .authorizedGrantTypes("authorization_code", "refresh_token") .scopes("read", "write") .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(7200) .redirectUris("http://localhost:8080/login/oauth2/code/my-client"); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager) .userDetailsService(userDetailsService); } } ``` 以上是整合OAuth2.0的一些步骤和代码示例。需要注意的是,OAuth2.0是一个复杂的协议,需要深入理解和熟练掌握。同时,需要根据实际业务需求进行相应的配置和开发。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值