Spring Security OAuth2:认证服务器实现

授权码模式

创建父工程

cloud-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.wj</groupId>
    <artifactId>cloud-oauth2-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>cloud-oauth2-base</module>
        <module>cloud-oauth2-auth-server</module>
    </modules>
    <packaging>pom</packaging>

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

    <properties>
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <mybatis-plus.version>3.2.0</mybatis-plus.version>
        <druid.version>1.1.12</druid.version>
        <kaptcha.version>2.3.2</kaptcha.version>
        <fastjson.version>1.2.8</fastjson.version>
        <commons-lang.version>2.6</commons-lang.version>
        <commons-collections.version>3.2.2</commons-collections.version>
        <commons-io.version>2.6</commons-io.version>
        <!-- 定义版本号, 子模块直接引用-->
        <oauth-security.version>1.0-SNAPSHOT</oauth-security.version>
    </properties>


    <!--依赖声明-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <!--maven不支持多继承,使用import来依赖管理配置-->
                <scope>import</scope>
            </dependency>
            <!--mybatis-plus启动器-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>
            <!--druid连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid.version}</version>
            </dependency>
            <!-- kaptcha 用于图形验证码 -->
            <dependency>
                <groupId>com.github.penggle</groupId>
                <artifactId>kaptcha</artifactId>
                <version>${kaptcha.version}</version>
            </dependency>
            <!-- 工具类依赖 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson.version}</version>
            </dependency>

            <dependency>
                <groupId>commons-lang</groupId>
                <artifactId>commons-lang</artifactId>
                <version>${commons-lang.version}</version>
            </dependency>
            <dependency>
                <groupId>commons-collections</groupId>
                <artifactId>commons-collections</artifactId>
                <version>${commons-collections.version}</version>
            </dependency>
            <dependency>
                <groupId>commons-io</groupId>
                <artifactId>commons-io</artifactId>
                <version>${commons-io.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <!--springboot 打包插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.2.4.RELEASE</version>
            </plugin>
        </plugins>
    </build>
</project>

创建子工程

cloud-oauth2-base

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">
    <parent>
        <artifactId>cloud-oauth2-parent</artifactId>
        <groupId>com.wj</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-oauth2-base</artifactId>

    <dependencies>
        <!--类中setter/getter,使用注解-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- 工具类依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>
    </dependencies>
</project>

创建通用返回类:com.wj.base.result.R

@Data
public class R implements Serializable {

    // 响应业务状态
    private Integer code;

    // 响应消息
    private String message;

    // 响应中的数据
    private Object data;

    public R() {
    }
    public R(Object data) {
        this.code = 200;
        this.message = "OK";
        this.data = data;
    }
    public R(String message, Object data) {
        this.code = 200;
        this.message = message;
        this.data = data;
    }

    public R(Integer code, String message, Object data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static R ok() {
        return new R(null);
    }
    public static R ok(String message) {
        return new R(message, null);
    }
    public static R ok(Object data) {
        return new R(data);
    }
    public static R ok(String message, Object data) {
        return new R(message, data);
    }

    public static R build(Integer code, String message) {
        return new R(code, message, null);
    }

    public static R build(Integer code, String message, Object data) {
        return new R(code, message, data);
    }

    public String toJsonString() {
        return JSON.toJSONString(this);
    }

    /**
     * JSON字符串转成 R 对象
     */
    public static R format(String json) {
        try {
            return JSON.parseObject(json, R.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

cloud-oauth2-auth-server

基本配置

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">
    <parent>
        <artifactId>cloud-oauth2-parent</artifactId>
        <groupId>com.wj</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-oauth2-auth-server</artifactId>

    <dependencies>
        <dependency>
            <artifactId>cloud-oauth2-base</artifactId>
            <groupId>com.wj</groupId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--spring mvc相关的-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Security、OAuth2 和JWT等 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <!-- 注册到 Eureka
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        -->

        <!-- redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--mybatis-plus启动器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

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

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

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.2.4.RELEASE</version>
                <configuration>
                    <mainClass>com.wj.oauth2.AuthServerApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

application.yml

server:
  port: 8090
  servlet:
    context-path: /auth
spring:
  datasource:
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/study-security?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8

springboot主启动类com.wj.oauth2.AuthServerApplication:

@SpringBootApplication
public class AuthServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthServerApplication.class, args);
    }

}
配置类

统一管理Bean配置类: SpringSecurityBean

@Configuration
public class SpringSecurityBean {
    @Bean  //引入PasswordEncoder
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

SpringSecurity配置类:

@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 内存方式存储用户信息,这里为了方便就不从数据库中查询了
        auth.inMemoryAuthentication().withUser("admin")
                .password(passwordEncoder.encode("1234"))
                .authorities("product");
    }
}

认证服务器配置类:AuthorizationServerConfig

@Configuration
@EnableAuthorizationServer//开启认证服务器功能
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**  配置被允许访问此认证服务器的客户端详情信息
     * 方式1:内存方式管理
     * 方式2:数据库管理
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 使用内存方式
        clients.inMemory()
                // 客户端id
                .withClient("wj-pc")
        // 客户端密码,要加密,不然一直要求登录
        .secret(passwordEncoder.encode("wj-secret"))
        // 资源id, 如商品资源
        .resourceIds("product-server")
        // 授权类型, 可同时支持多种授权类型
        .authorizedGrantTypes("authorization_code", "password", "implicit","client_credentials","refresh_token")
        // 授权范围标识,哪部分资源可访问(all是标识,不是代表所有)
        .scopes("all")
        // false 跳转到授权页面手动点击授权,true 不用手动授权,直接响应授权码,
        .autoApprove(false)
        .redirectUris("http://www.baidu.com/");// 客户端回调地址
    }
}

配置说明:

withClient:允许访问此认证服务器的客户端id , 如:PC、APP、小程序各不同的的客户端id。

secret:客户端密码,要加密存储,不然获取不到令牌一直要求登录,, 而且一定不能被泄露。

authorizedGrantTypes: 授权类型, 可同时支持多种授权类型:可配置:"authorization_code", "password", "implicit","client_credentials","refresh_token"

scopes:授权范围标识,如指定微服务名称,则只能访问指定的微服务。

autoApprove:false 跳转到授权页面手动点击授权,true 不用手动授权,直接响应授权码

redirectUris 当获取授权码后,认证服务器会重定向到这个URI,并且带着一个授权码code响应回来

令牌访问端点

Spring Security 对 OAuth2 默认提供了可直接访问端点,即URL:

/oauth/authorize:申请授权码 code, 涉及的类AuthorizationEndpoint

/oauth/token:获取令牌 token, 涉及的类TokenEndpoint/oauth/check_token:用于资源服务器请求端点来检查令牌是否有效, 涉及的类CheckTokenEndpoint

/oauth/confirm_access:用户确认授权提交, 涉及的类WhitelabelApprovalEndpoint

/oauth/error:授权服务错误信息, 涉及的类WhitelabelErrorEndpoint

/oauth/token_key:提供公有密匙的端点,使用 JWT 令牌时会使用 , 涉及的类TokenKeyEndpoint

测试

发送请求获取授权码code

访问:localhost:8090/auth/oauth/authorize?client_id=wj-pc&response_type=code

这里的client_id是在AuthorizationServerConfig中配置的。

输入账号密码进行登陆,账号和密码:admin/1234 ,是在SpringSecurityConfig中配置的

image-20210308172218218

登陆成功后,选择Approve,点击Authorize,这里跳转到www.baidu.com ,并且后面携带了code,这里的code就是授权码,后面我们就可以通过授权码来获取令牌(access_token)

image-20210308172256770

通过授权码获取令牌

使用postman测试:http://localhost:8090/auth/oauth/token

image-20210308172928031

这里的username和password是在AuthorizationServerConfig中配置的

设置为post请求,并设置请求体

image-20210308175033878

这里的grant_type是authorization_code,code是上一步获取的code

发送请求后,获得了access_token

image-20210308175522928

注意,code只能获取一次access_token,获取后就会失效,第二次获取就会失败

image-20210308175604772

密码授权模式

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己在服务提供商(认证服务器)上的用户名和密码,然后客户端通过用户提供的用户名和密码向服务提供商(认证服务器)获取令牌。

如果用户名和密码遗漏,服务提供商(认证服务器)无法判断客户端提交的用户和密码是否盗取来的,那意味着令牌就可随时获取,数据被丢失。

所以密码授权模式适用于产品都是企业内部的,用户名密码共享不要紧。如果是第三方这种不太适合。也适用手机APP提交用户名密码。

配置密码模式

修改SpringSecurityConfig配置类:

@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 内存方式存储用户信息
        auth.inMemoryAuthentication().withUser("admin")
                .password(passwordEncoder.encode("1234"))
                .authorities("product");
    }

    /**
     * password密码模式需要使用此认证管理器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

修改AuthorizationServerConfig类:

@Configuration
@EnableAuthorizationServer//开启认证服务器功能
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    /**  配置被允许访问此认证服务器的客户端详情信息
     * 方式1:内存方式管理
     * 方式2:数据库管理
     * localhost:8090/auth/oauth/authorize?client_id=wj-pc&response_type=code
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 使用内存方式
        clients.inMemory()
                // 客户端id
                .withClient("wj-pc")
        // 客户端密码,要加密,不然一直要求登录, 获取不到令牌, 而且一定不能被泄露
        .secret(passwordEncoder.encode("wj-secret"))
        // 资源id, 如商品资源
        .resourceIds("product-server")
        // 授权类型, 可同时支持多种授权类型
        .authorizedGrantTypes("authorization_code", "password", "implicit","client_credentials","refresh_token")
        // 授权范围标识,哪部分资源可访问(all是标识,不是代表所有)
        .scopes("all")
        // false 跳转到授权页面手动点击授权,true 不用手动授权,直接响应授权码,
        .autoApprove(false)
        .redirectUris("http://www.baidu.com/");// 客户端回调地址
    }

    /**
     * 重写父类的方法
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //密码模式需要设置此认证管理器
        endpoints.authenticationManager(authenticationManager);
    }
}

测试使用

使用postman测试:

访问:http://localhost:8090/auth/oauth/token

image-20210309092223859

填上请求参数:

image-20210309092340231

发送请求即可获取到access_token:

image-20210309092419351

简化模式

不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,不需要先获取授权码。直接可以一次请求就可得到令牌,在redirect_uri指定的回调地址中传递令牌( access_token )。该模式适合直接运行在浏览器上的应用,不用后端支持(例如 Javascript 应用)

注意:只需要客户端id,客户端密码都不需要。

浏览器直接访问:localhost:8090/auth/oauth/authorize?client_id=wj-pc&response_type=token

先登录:

image-20210309092818515

登陆成功并授权后,直接跳转到指定页面,并在uri中返回了access_token

image-20210309092920599

注意

  • 简化模式不允许按照 OAuth2 规范发布刷新令牌(refresh token)。这种行为是有必要的,它要求在使用运行在浏览器中的程序时,用户必须在场,这样可以在任何需要的时候,给第三方应用授权。

  • 当使用简化模式时,第三方应用始终需要通过重定向URI来注册,这样能确保不会将token传给不需要验证的客户端。如果不这样做,一些心怀不轨的用户可能先注册一个应用,然后试图让其他的应用来顶替,接收这个 token,这样可能导致灾难性的结果

客户端授权模式

客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向服务提供商(认证服务器)进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求服务提供商(认证服务器)提供服务,其实不存在授权问题。

  • 客户端向认证服务器进行身份认证,并要求一个访问令牌。

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

测试

post测试:http://localhost:8090/auth/oauth/token

image-20210309093435745

grant_type等于client_credentials

image-20210309093543715

发送请求返回了access_token,注意响应结果没有刷新令牌的

image-20210309093620031

原文地址:https://www.cnblogs.com/wwjj4811/p/14503898.html