权限-spring cloud OAuth2.0(六)

OAuth2.0

作用

OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。

OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。

举例说明OAuth2.0的作用。

答:比如我们登录腾讯软件,我们可以通过微信登录。腾讯软件系统通过微信获取用户的信息进行认证。用户自己决定是否同意腾讯软件是否统一使用微信进行登录。认证通过后,微信就会为腾讯生成一个令牌,腾讯通过这个令牌就能从微信拿取到用户信息(腾讯软件就表示第三方应用)

概念

OAuth2.0的服务提供方涵盖两个服务,即授权服务(Authorization Server,也叫认证服务)和资源服务(Resource Server)

使用Spring Security OAuth2的时候你可以选择把它们在同一个应用程序中实现,也可以选择建立使用同一个授权服务的多个资源服务。

**授权服务(Authorization Server ) **应包含对接入端以及登入用户的合法性进行验证并颁发token等功能,对令牌的请求端点由Spring MIVC控制器进行实现。

  • AuthorizationEndpoint服务于认证请求。默认URL : /oauth/authorize 。
  • TokenEndpoint服务于访问令牌的请求。默认URL : /oauth/token 。

资源服务(Resource Server)应包含对资源的保护功能,对非法请求进行拦截,对请求中token进行解析鉴权等。

  • OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。

认证资源

授权服务配置

授权服务器配置

我们可以用@EnableAuthorizationServer注解一个类,并且这个类继承AuthorizationServerConfigurerAdapter来配置OAuth2.0授权服务器。

AuthorizationServerConfigurerAdapter类实现了AuthorizationServerConfigurer类

@EnableAuthorizationServer注解会触发哪些配置

在官网中,很详细的说出了,当您使用这个注解的时候会触发3个重要的配置类
ClientDetailsServiceConfigurer 用于配置客户端信息
AuthorizationServerSecurityConfigurer 用于配置授权服务器端点的安全过滤拦截
AuthorizationServerEndpointsConfigurer 用于配置端点

查看AuthorizationServerConfigurerAdapter源码

public AuthorizationServerConfigurerAdapter() {}

public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}

public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}

public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}

通过源码我们知道了我们需要去配置的类(重要:我们关于oauth的配置就是基于这三点)

1. ClientDetailsServiceConfigurer类 
用来配置客服端详情,目前支持内存或者JDBC两种方式获取用户信息,(ClientDetails信息加载实现类)
简单理解:可用于比较客服端传过来的密钥,客服端id等是否和数据库中或内存中一致,作用有点类似于UserDetails
【表明那些客服端允许来授权服务,申请令牌(客服端信息是否合法)】

2. AuthorizationServerEndpointsConfigurer类
配置令牌的访问端点(实质就是url)和令牌服务(令牌怎么生成,令牌怎么存储等)

3. AuthorizationServerSecurityConfigurer类
用来配置令牌端点的安全约束
上面AuthorizationServerEndpointsConfigurer类暴露了一些令牌访问端点,这里就可以配置什么情况允许访问。

客服端详细信息

ClientDetailsServiceConfigurer能够使用内存或者JDBC来实现客户端详情服务

ClientDetailsService负责查找ClientDetails,而ClientDetails有几个重要的属性如下列表:

clientId : (必须的)用来标识客户的ld
secret : ((需要值得信任的客户端)客户端安全码,如果有的话。(相当于密码)
scope :用来限制客户端的访问范围(权限范围),如果为空(默认)的话,那么客户端拥有全部的访问范围。
authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
authorities :此客户端可以使用的权限(基于Spring Security authorities ) 

管理令牌(令牌的存储方式)

AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。

自己可以创建 AuthorizationServerTokenServices,这个接口的实现,则需要继承DefaultTokenServices这个类,里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个TokenStore接口来实现以外,这个类几乎帮你做了所有的事情。并且TokenStore这个接口有一个默认的实现,它就是InMemoryIokenStore ,如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面
有几个版本,它们都实现了TokenStore接口︰

InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。

JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的classpath当中。

JwtTokenStore:这个版本的全称是ISON Web Token (JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个企经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌

令牌访问端点的配置
AuthorizationServerEndpointsConfigurer这个对象的实例可以完成令牌服务以及令牌endpoint配置。

配置授权类型(Grant Types )

AuthorizationServerEndpointsConfigurer通过设定以下属性决定支持的授权类型(Grant Types ) :

1. authenticationManager:
认证管理器,当你选择了资源所有者密码( password )授权类型的时候,必须使用认证管理器。(密码模式)

2. userDetailsService:
如果你设置了这个属性的话,那说明你有一个自己的UserDetailsService 接口的实现,或者你可以把这个东西设置到全局域上面去(例如GlobalAuthenticationManagerConfigurer这个配置对象),当你设置了这个之后,那么"refresh_token"即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。(这个也是用于密码模式)

3. authorizationCodeServices:
这个属性是用来设置授权码服务的(即 AuthorizationCodeServices的实例对象),主要用于"authorization_code"授权码类型模式。(授权码类型模式)

4. implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。(隐式授权模式)

5. tokenGranter:
当你设置了这个东西(即TokenGranter接口实现),那么授权将会交由你来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你员求的时候,才会考虑使用这个。(一般不用)

配置授权端点的UR(Endpoint URLS)

AuthorizationServerEndpointsConfigurer这个配置对象有一个叫做pathMapping的方法用来配置端点URL链
接,它有两个参数︰

  1. 第一个参数:String类型的,这个端点URL的默认链接。
  2. 第二个参数:String类型的,你要进行替代的URL链接。

以上的参数都将以"/"为开始的字符串,框架的默认URL链接如下列表,可以作为这个pathMapping()方法的第一个参数:

端点的URL 作用
/oauth/authorize 授权端点
/oauth/token 令牌端点
/oauth/confirm_access 用户确认授权提交端点
/oauth/error 授权服务错误信息端点
/oauth/check_token 用于资源服务访问的令牌解析端点
/oauth/token_key 提供公有密匙的端点,如果你使用JWT令牌的话

需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问.在AuthorizationServer配置令牌访问端点。

令牌端点的安全约束
AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束.

上面的令牌端点有很多的URL,安全约束就是配置怎么去访问这些URL。

授权服务器的配置相关步骤

步骤 关键代码 所在类
1. 配置客服端详情服务ClientDetailsServiceConfigurer configure(ClientDetailsServiceConfigurer client) AuthorizationServer
2. 配置令牌的存储策略 public TokenStore tokenStore() TokenConfig
3. 配置令牌服务 AuthorizationServerTokenServices tokenServices() AuthorizationServer
4. 配置授权类型authorization_code AuthorizationCodeServices authorizationCodeServices() AuthorizationServer
5. 配置授权类型 password AuthenticationManager authenticationManagerBean() SecurityConfig
6.配置授权类型 password UserDetails loadUserByUsername(String username) UserDetailsServiceImpl
7. 配置安全拦截机制 configure(HttpSecurity http) SecurityConfig
8. 配置密码加密方式 PasswordEncoder passwordEncoder() SecurityConfig
9. 配置令牌端点 configure(AuthorizationServerEndpointsConfigurer endpoints) AuthorizationServer
10. 令牌端点的安全约束 configure(AuthorizationServerSecurityConfigurer security) AuthorizationServer

项目代码实现

项目父亲工程

pom.xml

<?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>org.dong.oauth</groupId>
    <artifactId>oauth</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>uaa</module>
        <module>eureka</module>
        <module>order</module>
    </modules>

    <!--打包方式  pom-->
    <packaging>pom</packaging>

    <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>
        <junit.version>4.12</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.16.18</lombok.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>0.2.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--springCloud的依赖-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--SpringBoot-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.4.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--数据库-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.17</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.10</version>
            </dependency>
            <!--SpringBoot 启动器-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>1.3.2</version>
            </dependency>
            <!--日志测试~-->
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-core</artifactId>
                <version>1.2.3</version>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
            </dependency>
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-jwt</artifactId>
                <version>1.0.10.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                <version>2.1.3.RELEASE</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

子工程uua用于授权服务

pom.xml

<?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>oauth</artifactId>
        <groupId>org.dong.oauth</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>uaa</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</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>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>


</project>

application.yml

spring:
  application:
    name: uaa-service
  main:
    allow-bean-definition-overriding: true
  datasource:
    url: jdbc:mysql://localhost:3306/security_demo01?userSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

server:
  port: 9001
  servlet:
    context-path: /uaa
  tomcat:
    remote-ip-header: x‐forwarded‐for
    protocol-header: x‐forwarded‐proto
  use-forward-headers: true

mybatis:
  #configuration和配置文件mybatis-config.xml只能指定一个
  configuration:
    #    开启驼峰命名
    map-underscore-to-camel-case: true
  type-aliases-package: org.dong.oauth.uaa.pojo
  mapper-locations: classpath:mapper/*.xml

logging:
  level:
    org.dong.oauth.uaa.dao: debug

uaa项目结构图

uaa项目结构图

AuthorizationServer类

package org.dong.oauth.uaa.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;


/**
 * 授权服务的配置
 * @EnableAuthorizationServer注解标明它是授权服务
 *AuthorizationServerConfigurerAdapter类implements了AuthorizationServerConfigurer类
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    //因为配置客服端详情的时候客服端的授权类型authorizedGrantTypes有password
    //所以我们需要配置AuthenticationManager和userDetailsService
    //这个认证管理器我配置到SecurityConfig类中
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 1. 配置客服端详情服务ClientDetailsServiceConfigurer
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer client) throws Exception {
        client.inMemory()//使用内存存储
                .withClient("c1")//with一个客户端 参数是客服端id
                .secret(new BCryptPasswordEncoder().encode("secret"))//密钥
                .resourceIds("res1")//客服端可以访问的资源id
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")//该客户端允许的授权类型
                .scopes("all")//允许的授权范围
                .autoApprove(false)//false会跳转到授权页面,让用户授权。true则不会
                .redirectUris("http://www.baidu.com");//客服端的回调地址
//        .and()
//        .withClient("id")
//        .....    这样就可以配置多个客服端了
    }

    /**
     * 2. 配置令牌服务
     * (存储方式见类TokenConfig类)
     * (令牌管理服务见下方tokenServices)
     */
    @Bean
        public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        //配置客服端详情服务
        services.setClientDetailsService(clientDetailsService);
        //支持刷新令牌
        services.setSupportRefreshToken(true);
        //令牌存储方式
        services.setTokenStore(tokenStore);
        //令牌有效时期为2小时
        services.setAccessTokenValiditySeconds(7200);
        //刷新令牌默认有效期3天
        services.setRefreshTokenValiditySeconds(259200);
        return services;
    }

    /**
     * 3. 配置令牌的访问端点
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                //认证管理器,当你选择了资源所有者密码( password )授权类型的时候,请设置这个属性注入一个AuthenticationManager对象。
                .authenticationManager(authenticationManager)
                //授权码服务
                .authorizationCodeServices(authorizationCodeServices)
                .tokenServices(tokenServices())//令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    /**
     * 4. 配置令牌端点的安全约束
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                //对应/oauth/token_key 的URL是公开的(不用登录就能访问)
                .tokenKeyAccess("permitAll()")
                ///oauth/check_token 是公开的
                .checkTokenAccess("permitAll()")
                //表单认证(用于客服端申请令牌)
                .allowFormAuthenticationForClients();
        //不公开的url就需要进行认证
    }


    /*
     *因为配置客服端详情的时候客服端的授权类型authorizedGrantTypes有authorization_code
     *所以需要配置authorizationCodeServices
     * 下面我们暂时采用内存的方式
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

}

TokenConfig类

package org.dong.oauth.uaa.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

@Configuration
    public class TokenConfig {
    /**
     * 存储令牌的三种方式
     * 1. JdbcTokenStore(jdbc方式)
     * 2. JWTTokenStore(JWT方式)
     * 3. InMemoryTokenStore(内存方式,下面我们使用内存方式存储普通令牌)
     * @return
     */
    @Bean
    public TokenStore tokenStore(){
        return new InMemoryTokenStore();
    }
}

SecurityConfig类

package org.dong.oauth.uaa.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /*
     *在AuthorizationService类中因为配置客服端详情时,
     * 客服端授权类型有password类型,
     * 所以需要配置AuthenticationManager和userDetailsService
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

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

    //安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .anyRequest().permitAll()//其它请求都允许 注意位置(放在最前面所有请求都会被放行)
                .anyRequest().authenticated()
                .and()
                .formLogin();
    }
}

userDao

package org.dong.oauth.uaa.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.dong.oauth.uaa.pojo.Permission;
import org.dong.oauth.uaa.pojo.UserDto;
import org.springframework.stereotype.Repository;

import java.util.List;


@Repository
@Mapper
public interface UserDao {
    //根据用户名查询用户信息
    @Select("select * from user where username=#{username}")
    UserDto findUserDetailByUserName(String username);

    //根据用户的id查询用户的权限
    List<Permission> findPermissionByUserId(UserDto userDto);
}

Permission

package org.dong.oauth.uaa.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Permission {
    private Integer id;
    private String code;//权限标识符
    private String description;//描述
    private String url;// 请求地址
}

UserDto

package org.dong.oauth.uaa.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {
    private Integer id;
    private String username;//账号
    private String password;
    private String realName;//用户名
    private String mobile;
}

UserDetailsServiceImpl

package org.dong.oauth.uaa.service.impl;

import org.dong.oauth.uaa.dao.UserDao;
import org.dong.oauth.uaa.pojo.Permission;
import org.dong.oauth.uaa.pojo.UserDto;
import org.springframework.security.core.userdetails.User;
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 javax.annotation.Resource;
import java.util.List;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    /**
     * 根据账号查询用户信息
     * 通过接口的方式实现,可以替代继承WebSecurityConfigurerAdapter类的配置类的UserDetailsService方法
     * 源码中:DaoAuthenticationProvider类会根据账户名来调用这个方法获得用户详细信息。
     */
    @Resource
    UserDao userDao;


    /*
     *在AuthorizationService类中因为配置客服端详情时,
     * 客服端授权类型有password类型,
     * 所以需要配置AuthenticationManager和userDetailsService
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //从数据库中查出来的用户
        UserDto user=userDao.findUserDetailByUserName(username);
        if (user==null){
            //只要返回null,就会由DaoAuthenticationProvider类抛出异常
            return null;
        }
        //根据用户的id查询用户的权限
        List<Permission> permissions=userDao.findPermissionByUserId(user);
        //authorities需要的是一个数组
        String[] authorities=new String[permissions.size()];
        for (int i = 0; i < permissions.size(); i++) {
            authorities[i]=permissions.get(i).getCode();
        }
        UserDetails userDetails= User.withUsername(user.getUsername()).password(user.getPassword()).authorities(authorities).build();
        return userDetails;
    }
}

启动类

package org.dong.oauth.uaa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients("org.dong.oauth.uaa")
public class UaaService {
    public static void main(String[] args) {
        SpringApplication.run(UaaService.class);
    }
}

userMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace:命名空间,用于隔离sql,还有一个很重要的作用,后面会讲 -->
<mapper namespace="org.dong.oauth.uaa.dao.UserDao">
    <!-- 根据用户的id查询用户的权限-->
    <select id="findPermissionByUserId" parameterType="UserDto" resultType="Permission">
        SELECT * FROM `permission` WHERE id IN(
            SELECT `permission_id` FROM `role_permission` WHERE `role_id` IN(
                SELECT `role_id` FROM `user_role` where `user_id`=#{id}
            )
        )
    </select>
</mapper>

数据库见前面的文章权限-基于Mysql实现security(四)

授权类型测试

1 授权码模式 authorization_code

授权码流程

(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将被浏览器重定向到授权服务器,重定向时会附加客户端的身份信息。如:(response_type=code是固定的)

localhost:9001/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

启动程序后,访问上面的地址会跳转到springsecurity的登录页面。(dong 123456)

登录成功后就会跳转到该页面

资源拥有者授权页面

选中Approve,点击Authorize会跳转到百度页面,然后获取code

资源拥有者授权成功后跳转

参数列表如下:

参数 详情
client_id 客户端准入标识(客服id)
response_type 授权码模式固定为code
scope 客户端权限
redirect_uri 跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)

( 2)浏览器中出现向授权服务器授权的页面,之后将用户同意授权。
( 3)授权服务器将授权码(AuthorizationCode )转经浏览器发送给client(通过redirect_uri)。

( 4)客户端拿着授权码向授权服务器索要访问access_token,请求如下:

http://localhost:9001/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=I89tse&redirect_uri=http://www.baidu.com
参数 详情
client_secret 客户端秘钥
grant_type 授权类型,填写authorization_code,表示授权码模式
code 授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri 申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

打开postman,访问上面的地址

客服端请求token

结果: 一个授权码code只能够使用一次,失效了需要重新通过资源拥有者授权。

{
    "access_token": "7c268900-81da-4194-bbd3-78ac030375ae",
    "token_type": "bearer",
    "refresh_token": "7e0aa909-7069-4447-af1b-821d2da8eacd",
    "expires_in": 7199,
    "scope": "all"
}

(5)授权服务器返回令牌(access_token)

授权码模式是四种模式中最安全的一种模式。一般用于client是Web服务器端应用或第三方的原生App调用资源服务 的时候。因为在这种模式中access_token不会经过浏览器或移动端的App,而是直接从服务端去交换,这样就最大 限度的减小了令牌泄漏的风险。

2 简化模式

下图是简化模式交互图:

简化模式交互图

(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。如:

http://localhost:9001/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com

访问上面程序还是先会跳到spring security自带的登录页面

登录成功后会访问如下页面

简单模式授权

选中Approve 点击Authorize。会跳转到百度页面,并且直接获取通过令牌

简单模式跳转

注意response_type=token,说明是简化模式。

(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。

(3)授权服务器将授权码将令牌(access_token)以Hash的形式存放在重定向uri的fargment中发送给浏览

​ 器。

注:fragment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的末尾通过 (#)作为 fragment 的开头,

其中 # 不属于 fragment 的值。如https://domain/index#L18这个 URI 中 L18 就是 fragment 的值。大家只需要

知道js通过响应浏览器地址栏变化的方式能获取到fragment 就行了。

一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。

密码模式 password

密码模式交互图

密码模式交互图

(1)资源拥有者将用户名、密码发送给客户端

(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),请求如下:

打开postman访问下面链接

http://localhost:9001/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&redirect_uri=http://www.baidu.com&username=dong&password=123456

密码模式uri

结果

{
    "access_token": "7c268900-81da-4194-bbd3-78ac030375ae",
    "token_type": "bearer",
    "refresh_token": "7e0aa909-7069-4447-af1b-821d2da8eacd",
    "expires_in": 2253,
    "scope": "all"
}

参数列表如下:

client_id:客户端准入标识。

client_secret:客户端秘钥。

grant_type:授权类型,填写password表示密码模式

username:资源拥有者用户名。

password:资源拥有者密码。

(3)授权服务器将令牌(access_token)发送给client

这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于client是我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。

客户端模式

客服端模式交互图

(1)客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)

(2)确认客户端身份无误后,将令牌(access_token)发送给client,请求如下:

http://localhost:9001/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials

客服端模式uri

结果如下

{
    "access_token": "6d102270-7161-4ee1-bf73-ee1d27179a7d",
    "token_type": "bearer",
    "expires_in": 7199,
    "scope": "all"
}

参数列表如下:

client_id:客户端准入标识。

client_secret:客户端秘钥。

grant_type:授权类型,填写client_credentials表示客户端模式

这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的。因此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。

资源服务测试(order)

order模块基本环境搭建

pom.xml

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId></dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</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>

application.yml

spring:
  application:
    name: order-service
  datasource:
    url: jdbc:mysql://localhost:3306/security_demo01?userSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  main:
    allow-bean-definition-overriding: true

server:
  port: 9002
  tomcat:
    remote-ip-header: x‐forwarded‐for
    protocol-header: x‐forwarded‐proto
  use-forward-headers: true
  servlet:
    context-path: /order

mybatis:
  #configuration和配置文件mybatis-config.xml只能指定一个
  configuration:
    #    开启驼峰命名
    map-underscore-to-camel-case: true
  type-aliases-package: org.dong.oauth.order.pojo
  mapper-locations: classpath:mapper/*.xml

logging:
  level:
    org.dong.oauth.order.dao: debug

启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class OrderService {
    public static void main(String[] args) {
        SpringApplication.run(OrderService.class);
    }
}

基础环境搭建完毕后,首先我们可以在order资源子模块的controller中编写一些资源

(在数据库中dong用户是拥有p1和p2权限的,密码是123456)

package org.dong.oauth.order.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {
    @GetMapping(value = "/r1")
    @PreAuthorize("hasAnyAuthority('p1')")
    public String r1(){
        return "访问资源1";
    }

    @GetMapping(value = "/r2")
    @PreAuthorize("hasAnyAuthority('p2')")
    public String r2(){
        return "访问资源2";
    }

    @GetMapping(value = "/r3")
    @PreAuthorize("hasAnyAuthority('p3')")
    public String r3(){
        return "访问资源3";
    }
}

我们需要拿着令牌来访问上面的资源,于是我们需要配置资源服务

@EnableResourceServer 注解到一个 @Confifiguration 配置类上,并且必须使用 ResourceServerConfifigurer 这个配置对象来进行配置(可以选择继承自 ResourceServerConfifigurerAdapter 然后覆写其中的方法,参数就是这个 对象的实例)

下面是一些可以配置的属性:

ResourceServerSecurityConfifigurer中主要包括:

  1. tokenServices:ResourceServerTokenServices 类的实例,用来实现令牌服务。
  2. tokenStore:TokenStore类的实例,指定令牌如何访问,与tokenServices配置可选
  3. resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务中进行验证。

其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌。

HttpSecurity配置这个与Spring Security类似:

请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是保护资源服务的全部路径。

通过http.authorizeRequests()来设置受保护资源的访问规则

其他的自定义权限保护规则通过 HttpSecurity 来进行配置。

@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链

package org.dong.oauth.order.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;

@Configuration
//表明是资源服务
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    /**
     * 1. 资源id需要和uaa模块中AuthorizationServer配置的资源id一致
     * .resourceIds("res1")//客服端可以访问的资源id
     */
    public static final String RESOURCE_ID = "res1";

    /**
     * 2. 安全校验
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources
                .resourceId(RESOURCE_ID) //资源id
                .tokenServices(tokenService()) //验证令牌的服务
                .stateless(true);
    }

    /**
     * 3. 安全拦截机制
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/**")
                .access("#oauth2.hasScope('all')")
                .and()
                .csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    /**
     * 4 资源服务令牌解析服务
     * 通过调用资源服务来校验令牌
     */
    @Bean
    public ResourceServerTokenServices tokenService() {
        //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service=new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:9001/uaa/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }

}

令牌解析方法:

  1. 使用 DefaultTokenServices 在资源服务器本地配置令牌存储、解码、解析方式使用
  2. RemoteTokenServices 资源服务器通过 HTTP 请求来解码令牌,每次都请求授权服务器端点 /oauth/check_token

使用授权服务的 /oauth/check_token 端点你需要在授权服务将这个端点暴露出去,以便资源服务可以进行访问。

小测试 这个配置类中的ResourceServerTokenServices是用来校验令牌的。我们可以通过上面的密码授权模式获取token,然后访问http://localhost:9001/uaa/oauth/check_token传入token,我们就能获取到一些信息。

运行程序uua使用密码校验

密码模式uri

结果:

{
    "access_token": "a75e73aa-6429-4576-82f6-1d41a52e0056",
    "token_type": "bearer",
    "refresh_token": "ca136cbe-7aae-4e8e-8806-6aff84ef524a",
    "expires_in": 5103,
    "scope": "all"
}

然后访问上面配置类中ResourceServerTokenServices的setCheckTokenEndpointUrl属性中的url

令牌校验

结果

{
    "aud": [
        "res1"
    ],
    "exp": 1620790861,
    "user_name": "dong",
    "authorities": [
        "p1",
        "p2"
    ],
    "client_id": "c1",
    "scope": [
        "all"
    ]
}

测试

同时运行uua和order

还是通过上面的密码授权模式访问(也可以通过其它授权模式访问)【密码授权模式访问步骤见上面的小测试】

然后范围controller中的资源

上面的程序访问成功,但是当我们访问r3是,dong没有p3权限,(我的程序是拒绝访问,但是参考教程讲解是出现了这个问题,具体原因我也不清楚。因此还是把这个细节写下来了)但是结果还是访问成功了。推荐文章

所以我们还需要写一个config类来解决这个问题

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().
                disable()
                .authorizeRequests()
                //所有/r/**的请求必须认证通过
                .antMatchers("/r/**").authenticated()
                .anyRequest().permitAll();
    }
}

修改后

没有p3权限拒绝访问

参考教程:黑马

posted @ 2021-05-12 10:56  懒鑫人  阅读(455)  评论(0)    收藏  举报