SpringSecurity Oauth2.0认证授权

OAuth2.0 中四种授权方式:

  1.授权码模式( authorization code):
    流程
      说明:【A服务客户端】需要用到【B服务资源服务】中的资源
      第一步:【A服务客户端】将用户自动导航到【B服务认证服务】,这一步用户需要提供一个回调地址,以备【B服务认证服务】返回授权码使用。
      第二步:用户点击授权按钮表示让【A服务客户端】使用【B服务资源服务】,这一步需要用户登录B服务,也就是说用户要事先具有B服务的使用权限。
      第三步:【B服务认证服务】生成授权码,授权码将通过第一步提供的回调地址,返回给【A服务客户端】。
        注意这个授权码并非通行【B服务资源服务】的通行凭证。
      第四步:【A服务认证服务】携带上一步得到的授权码向【B服务认证服务】发送请求,获取通行凭证token。
      第五步:【B服务认证服务】给【A服务认证服务】返回令牌token和更新令牌refresh token。
    使用场景
      授权码模式是OAuth2中最安全最完善的一种模式,应用场景最广泛,可以实现服务之间的调用,常见的微信,QQ等第三方登录也可采用这种方式实现。
  2.简化模式(implicit):
    流程
      说明:简化模式中没有【A服务认证服务】这一部分,全部由【A服务客户端】与B服务交互,整个过程不再有授权码,token直接暴露在浏览器。
      第一步:【A服务客户端】将用户自动导航到【B服务认证服务】,这一步用户需要提供一个回调地址,以备【B服务认证服务】返回token使用,还会携带一个【A服务客户端】的状态标识state。
      第二步:用户点击授权按钮表示让【A服务客户端】使用【B服务资源服务】,这一步需要用户登录B服务,也就是说用户要事先具有B服务的使用权限。

      第三步:【 B服务认证服务】生成通行令牌token,token将通过第一步提供的回调地址,返回给【A服务客户端】。
    使用场景
      适用于A服务没有服务器的情况。比如:纯手机小程序,JavaScript语言实现的网页插件等。
  3.密码模式(resource owner password credentials):
    流程
      第一步:直接告诉【A服务客户端】自己的【B服务认证服务】的用户名和密码
      第二步:【A服务客户端】携带【B服务认证服务】的用户名和密码向【B服务认证服务】发起请求获取token。
      第三步:【B服务认证服务】给【A服务客户端】颁发token。
    使用场景
      此种模式虽然简单,但是用户将B服务的用户名和密码暴露给了A服务,需要两个服务信任度非常高才能使用。
  4.客户端模式(client credentials):
    流程
      说明:这种模式其实已经不太属于OAuth2的范畴了。A服务完全脱离用户,以自己的身份去向B服务索取token。换言之,用户无需具备B服务的使用权也可以。完全是A服务与B服务内部的交互,与用户无关了。
      第一步:A服务向B服务索取token。
      第二步:B服务返回token给A服务。
    使用场景
      A服务本身需要B服务资源,与用户无关。

OAuth2.0 中表结构:

CREATE TABLE `oauth_access_token` (
  `token_id` varchar(255) DEFAULT NULL,
  `token` longblob,
  `authentication_id` varchar(255) DEFAULT NULL,
  `user_name` varchar(255) DEFAULT NULL,
  `client_id` varchar(255) DEFAULT NULL,
  `authentication` longblob,
  `refresh_token` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_approvals` (
  `userId` varchar(255) DEFAULT NULL,
  `clientId` varchar(255) DEFAULT NULL,
  `scope` varchar(255) DEFAULT NULL,
  `status` varchar(10) DEFAULT NULL,
  `expiresAt` datetime DEFAULT NULL,
  `lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(255) NOT NULL,
  `resource_ids` varchar(255) DEFAULT NULL,
  `client_secret` varchar(255) DEFAULT NULL,
  `scope` varchar(255) DEFAULT NULL,
  `authorized_grant_types` varchar(255) DEFAULT NULL,
  `web_server_redirect_uri` varchar(255) DEFAULT NULL,
  `authorities` varchar(255) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(255) DEFAULT NULL,
  `autoapprove` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_client_token` (
  `token_id` varchar(255) DEFAULT NULL,
  `token` longblob,
  `authentication_id` varchar(255) DEFAULT NULL,
  `user_name` varchar(255) DEFAULT NULL,
  `client_id` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_code` (
  `code` varchar(255) DEFAULT NULL,
  `authentication` varbinary(2550) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_refresh_token` (
  `token_id` varchar(255) DEFAULT NULL,
  `token` longblob,
  `authentication` longblob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  OAuth2.0 实战案例环境搭建:

    1.创建父工程,并引入坐标

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

    <properties>
        <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    2.创建资源模块,引入坐标

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</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>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>

    3.编写资源模块配置文件

server:
  port: 9002
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///springsecurity
    username: root
    password: root
  main:
    allow-bean-definition-overriding: true #允许覆盖spring放入到IOC容器的对象
mybatis:
  type-aliases-package: com.fgy.domain
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.fgy: debug

    4.编写启动类

@SpringBootApplication
@MapperScan("com.fgy.mapper")
public class OauthSourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OauthSourceApplication.class, args);
    }
}

    5.编写处理器

@RestController
@RequestMapping("/product")
public class ProductController {
    @GetMapping("/findAll")
    public String findAll(){
        return "查询产品列表成功!";
    }
}

    6.启动项目测试

      由于此刻,项目中添加的有SpringBoot的Security包,默认不通过认证是无法访问处理器的,会跳转到security默认的登录页面。

  将访问资源作为 OAuth2 的资源来管理:

    1.编写实体类

public class SysRole implements GrantedAuthority {

    private Integer id;
    private String roleName;
    private String roleDesc;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getRoleDesc() {
        return roleDesc;
    }

    public void setRoleDesc(String roleDesc) {
        this.roleDesc = roleDesc;
    }

    @JsonIgnore // 在将来我们可能会拿这个实体类转成json字符串,或者json转成java对象,需要忽略重写的几个属性
    @Override
    public String getAuthority() {
        return roleName;
    }
}
SysRole
public class SysUser implements UserDetails {

    private Integer id;
    private String username;
    private String password;
    // 在MySql中是没有直接定义成Boolean这种数据类型.
    // 它只能定义成 tinyint(1) ;
    // status等于1时代表true,status等于0时代表false;
    private Boolean status;
    private List<SysRole> roles;

    public List<SysRole> getRoles() {
        return roles;
    }

    public void setRoles(List<SysRole> roles) {
        this.roles = roles;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Boolean getStatus() {
        return status;
    }

    public void setStatus(Boolean status) {
        this.status = status;
    }

    @JsonIgnore // 在将来我们可能会拿这个实体类转成json字符串,或者json转成java对象,需要忽略重写的几个属性
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles;
    }

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

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

    /**
     * 账户是否失效
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 账户是否锁定
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 密码是否失效
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 指定当前用户是否可用
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return status;
    }
}
SysUser

    2.编写资源管理配置类,放在config包下

@Configuration
@EnableResourceServer
public class OauthSourceConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    /**
     * 指定token的持久化策略
     * InMemoryTokenStore表示将token存储在内存
     * RedisTokenStore表示将token存储在redis中
     * JdbcTokenStore存储在数据库中
     *
     * @return
     */
    @Bean
    public TokenStore jdbcTokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    /**
     * 指定当前资源的id和存储方案
     * product_api 建议放在配置文件中,通过读取配置文件来使用,方便修改
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("product_api").tokenStore(jdbcTokenStore());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            // 指定不同请求方式访问资源所需要的权限,一般查询是read,其余是write。
            .antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
            .antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
            .antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
            .antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
            .antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
            .antMatchers("/**").hasAnyRole("ROLE_USER", "ROLE_ADMIN")
             .and()
            .headers().addHeaderWriter((request, response) -> {
                response.addHeader("Access-Control-Allow-Origin", "*");// 允许跨域
                if (request.getMethod().equals("OPTIONS")) {// 如果是跨域的预检请求,则原封不动向下传达请求头信息
                    response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access-Control - Request - Method"));
                    response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control - Request - Headers"));
                }
            });
    }
}

  创建授权模块 oauth_server

    1.创建工程并引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</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>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.1.5</version>
</dependency>

    2.编写配置文件

server:
  port: 9000
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///springsecurity
    username: root
    password: root
  main:
    allow-bean-definition-overriding: true #允许覆盖spring放入到IOC容器的对象
mybatis:
  type-aliases-package: com.fgy.domain
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.fgy: debug

    3.编写启动类

@SpringBootApplication
@MapperScan("com.fgy.mapper")
public class OauthServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(OauthServerApplication.class, args);
    }
}

    4.将SpringBoot整合SpringSecurity认证的代码复制过来

      

     5.编写SpringSecurity配置类,放在config包下

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    /**
     * 生成认证使用的加密对象,添加到容器中
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 认证用户的来源
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    // 配置springSecurity相关信息
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .permitAll()
                .and()
                .csrf()
                .disable();
    }

    // AuthenticationManager对象在OAuth2认证服务中要使用,提取放入IOC容器中
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

    6.编写 OAuth2 授权配置类,放在config包下

@Configuration
@EnableAuthorizationServer
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {
    // 数据库连接池对象
    @Autowired
    private DataSource dataSource;
    // 授权模式专用对象
    @Autowired
    private AuthenticationManager authenticationManager;
    // 认证业务对象
    @Autowired
    private UserService userService;

    // 从数据库中查询出客户端信息
    @Bean
    public JdbcClientDetailsService clientDetailsService() {
        return new JdbcClientDetailsService(dataSource);
    }

    // token保存策略
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    // 授权信息保存策略
    @Bean
    public ApprovalStore approvalStore() {
        return new JdbcApprovalStore(dataSource);
    }

    // 授权码模式数据来源
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    // 指定客户端登录信息来源
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService());
    }

    // 检查token的策略
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.allowFormAuthenticationForClients();
        oauthServer.checkTokenAccess("isAuthenticated()");
    }

    // oauth2的主配置信息
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.approvalStore(approvalStore()) // 授权信息保存策略
                .authenticationManager(authenticationManager) // 授权模式专用对象
                .authorizationCodeServices(authorizationCodeServices()) // 授权码模式数据来源
                .tokenStore(tokenStore()); // token保存策略
    }
}

  测试准备:

    在数据库中手动添加客户端信息

    所有要使用当前项目资源的项目,都是我们的客户端。比如之前举的例子,A服务打印照片,B服务存储照片。
    A服务要使用B服务的资源,那么A服务就是B服务的客户端。
    这里要区分用户的信息和客户端信息,用户信息是用户在B服务上注册的用户信息,在sys_user表中。
    客户端信息是A服务在B服务中注册的账号,在OAuth2的oauth_client_details表中。
    测试数据sql语句如下:

INSERT INTO `oauth_client_details` (
  `client_id`,
  `resource_ids`,
  `client_secret`,
  `scope`,
  `authorized_grant_types`,
  `web_server_redirect_uri`,
  `authorities`,
  `access_token_validity`,
  `refresh_token_validity`,
  `additional_information`,
  `autoapprove`
) 
VALUES
  (
    'one',
    'product_api',
    '$2a$10$StAmhWCRMtOXhCiotGa2DOLjrbiPdYyt4kpQ7NxUzisJRBa7brQfu', -- aaa
    'read,write',
    'client_credentials,implicit,authorization_code,password,refresh_token',
    'http://www.baidu.com',
    NULL,
    NULL,
    NULL,
    NULL,
    'false'
  ) ;

  授权码模式测试:

    在地址栏访问:http://localhost:9000/oauth/authorize?response_type=code&client_id=one

    跳转到SpringSecurity默认认证页面,提示用户登录个人账户【这里是sys_user表中的数据】,因为用户要访问A系统,必须先登录B系统

      

     登录成功后询问用户是否给予操作资源的权限, Approve是授权,Deny是拒绝。(在数据库表 oauth_client_details 中 autoapprove 的值必须为false才会出现询问页面,否则直接可以访问)

    点击 Authorize 后跳转到回调地址并获取授权码(为了安全起见,授权码默认只能使用一次,重复使用会提示授权码无效

      

    使用授权码到服务器申请通行令牌 token(用户账户可以不输入,因为用户已经登录了B系统)

      

       

     携带通行令牌去访问资源服务器

      

   简化模式测试:

    在地址栏访问:http://localhost:9000/oauth/authorize?response_type=token&client_id=one

    由于上面用户已经登录过了,所以无需再次登录,其实和上面是有登录步骤的,这时,浏览器直接返回了token

      https://www.baidu.com/#access_token=376558f9-a260-415b-89a0-865dd195828b&token_type=bearer&expires_in=42231&scope=read%20write

      

     携带通行令牌去访问资源服务器

      

  密码模式测试:

    申请token

      

       

     携带通行令牌去访问资源服务器

      

   客户端模式测试:(申请令牌时,返回的内容没有刷新token,因为A服务与B服务内部的交互,与用户无关)

    申请token

      

       

     携带通行令牌去访问资源服务器

      

  测试刷新token:

    修改 OAuth2 授权配置类 OauthServerConfig 的主配置信息

// oauth2的主配置信息
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.userDetailsService(userService)
            .approvalStore(approvalStore()) // 授权信息保存策略
            .authenticationManager(authenticationManager) // 授权模式专用对象
            .authorizationCodeServices(authorizationCodeServices()) // 授权码模式数据来源
            .tokenStore(tokenStore()); // token保存策略
}

    

     

   权限相关测试:

    在资源模块配置类中开启权限控制的注解支持

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OauthSourceConfig extends ResourceServerConfigurerAdapter {
    ......
}

    在对应类或者方法上添加注解控制权限

@RestController
@RequestMapping("/product")
public class ProductController {

    @PreAuthorize("hasAnyRole('ROLE_PRODUCT', 'ROLE_ADMIN')")
    @GetMapping("/findAll")
    public String findAll(){
        return "查询产品列表成功!";
    }
}

    测试......

猜你喜欢

转载自www.cnblogs.com/roadlandscape/p/12543848.html