springsecurity+oauth2.0实现授权码模式

1.oauth2.0的简述

先说OAuth,OAuth是Open Authorization的简写。OAuth协议为用户资源的授权提供一个安全的,开放而又简易的标准。与以往的授权方式不同之处是OAuth的授权不会使第三方触及到用户的账号信息,即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAuth是安全的。

2.了解授权码模式

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

  • 第一步,A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接

https://b.com/oauth/authorize?response_type=code&client_id=CLIENT_ID

​ 上面 URL 中,response_type参数表示要求返回授权码(code),client_id参数让 B 知道是谁在请求。

springsecurity+oauth2.0实现授权码模式_第1张图片

  • 第二步,用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户选择同意,则B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码。

    https://a.com/?code=AUTHORIZATION_CODE

    上面 URL 中,code参数就是授权码。

springsecurity+oauth2.0实现授权码模式_第2张图片

  • 第三步,A网站拿到授权码后,即可向B网站请求令牌。

http://b.com/oauth/token?grant_type=authorization_code&code=O6Wn5w&client_secret=SECRET&client_id=CLIENT_ID

​ 上面 URL 中,grant_type参数表示采用授权码模式获取token令牌,code参数就是授权码,client_sercret参数是用户的密码,client_id参数指谁在请求。

springsecurity+oauth2.0实现授权码模式_第3张图片

  • 第四步,B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。

    {
        "access_token": "088dd9b2-a49d-457b-a1cc-4bb9f0e932f6",
        "token_type": "bearer",
        "refresh_token": "beb688c6-8b02-40fc-be74-fc1a3e4c07aa",
        "expires_in": 43199,
        "scope": "read write"
    }
    

    上面 JSON 数据中,access_token字段就是令牌,A 网站在后端拿到了

springsecurity+oauth2.0实现授权码模式_第4张图片

  • 最后,A网站拿到token令牌后,即可携带令牌访问网站资源端了

3.案例实战

(1)首先需要了解OAuth2.0中的表结构说明

  • oauth_client_details【核心表】
  • oauth_client_token
  • oauth_access_token
  • oauth_refresh_token
  • oauth_code

因为案例中主要用到的表是oauth_client_details,在这里主要介绍这张核心表,其他的表的功能可以到该网站查看

https://andaily.com/spring-oauth-server/db_table_description.html

oauth_client_details表说明

字段名 字段说明
client_id 主键,必须唯一,用于唯一标识每一个客户端
resource_ids 客户端所能访问的资源id集合
client_secret 用于指定客户端的访问密匙
scope 指定客户端申请的权限范围,可选值包括read,write,trust
authorized_grant_types 指定客户端支持的grant_type,可选值包括 authorization_code,password,refresh_token,implicit,client_credentials
web_server_redirect_uri 客户端的重定向URI
authorities 指定客户端所拥有的Spring Security的权限值
access_token_validity 设定客户端的access_token的有效时间值
additional_information 这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的 数据
archived 用于标识客户端是否已存档(即实现逻辑删除),默认值为’0’(即未存档)
trusted 设置客户端是否为受信任的,默认为’0’
autoapprove 设置用户是否自动Approval操作, 默认值为 ‘false’

(2)建表操作

  • 了解OAuth2.0的表功能后,我们需要创建这五张表,可到官方sql地址下载

https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

  • 除此之外,还需要创建用户表,角色表,用户角色关系表来实现用数据库信息认证

    用户表(sys_user)

id username password
1 admin $2a 10 10 10xQjca8d71Pkx2xPmasouQemol15C5KNn814B0cnok2o8FNK7lQCia
2 user $2a 10 10 10xQjca8d71Pkx2xPmasouQemol15C5KNn814B0cnok2o8FNK7lQCia

要注意的地方,密码是以BCryptPasswordEncoder加密保存的,我这里的明文是123,但是生成加密密文是随机的,所以在设置密码的时候,需要自己去动手生成属于自己的加密密文

System.out.println(new BCryptPasswordEncoder().encode("passsword"));

角色表(sys_role)

ID ROLE_NAME ROLE_DESC
1 ROLE_USER 基本角色
2 ROLE_ADMIN 超级管理员

用户角色关系表(sys_user_role)

user_id role_id
1 1
1 2
2 1

(3)创建maven项目

  1. 编写pom.xml
<dependencies>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-securityartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-oauth2artifactId>
        <version>2.2.0.RELEASEversion>
    dependency>
    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>5.1.47version>
    dependency>
    
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-boot-starterartifactId>
        <version>3.4.0version>
    dependency>
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plusartifactId>
        <version>3.4.0version>
    dependency>
    
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
    dependency>
dependencies>
  1. 父工程需要创建两个子模块,分别是服务端server_oauth和资源端source_oauth
    springsecurity+oauth2.0实现授权码模式_第5张图片
    springsecurity+oauth2.0实现授权码模式_第6张图片
  2. yml配置文件
server:
  port: 9001
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
    username: root
    password: 123456
  main:
    allow-bean-definition-overriding: true
logging:
  level:
    com.itheima: debug
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
  type-aliases-package: com.yanc.domain
  1. 服务端配置类
  • OauthServerConfig
@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());
    }
}
  • WebSecurityConfig
  1. @Configuration:设置为配置类

  2. @EnableWebSecurity:AOP拦截器

    • 加载了WebSecurityConfiguration配置类, 配置安全认证策略。
    • 加载了AuthenticationConfiguration, 配置了认证信息。
  3. @EnableGlobalMethodSecurity(prePostEnabled = true)

  • 作用:开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认。
//AOP,拦截器
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;


    //设置权限
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .permitAll()
                .and()
                .csrf()
                .disable();

    }

    //注入PasswordEncoder
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //指定认证对象的来源
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    //AuthenticationManager对象在Oauth2认证服务中要使用,提取放到IOC容器中
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
  1. 资源端配置类

OauthSourceConfig

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true)
public class OauthSourceConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

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

    /**
     * 指定当前的资源id和存储方案
     * @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')")
                .and()
                .headers().addHeaderWriter((request, response) -> {
            response.addHeader("Access-Control-Allow-Origin", "*");//允许跨域
            if (request.getMethod().equals("OPTIONS")) {//如果是跨域的预检请求,则原封不动向下传达请求头信息
                response.setHeader("Access-Control-Allow-Methods", request.getHeader("AccessControl-Request-Method"));
                response.setHeader("Access-Control-Allow-Headers", request.getHeader("AccessControl-Request-Headers"));
            }
        });
    }
}
  1. 编写实体类SysUser.java(此处使用了 Lombok 简化代码)
@Data
public class SysUser implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Integer status;
    @TableField(exist = false)
    private List<SysRole> roles;

    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles;
    }

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

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

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

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

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

    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  1. 编写实体类SysRole.java
@Data
public class SysRole  implements GrantedAuthority{

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

    @JsonIgnore
    @Override
    public String getAuthority() {
        return roleName;
    }
}
  1. 编写UserMapper(根据用户名查询,并封装成用户对象)
public interface UserMapper {

    @Select("select * from sys_user where username = #{username}")
    @Results({
            @Result(id = true,property = "id", column = "id"),
            @Result(property = "roles",column = "id",javaType = List.class,
                many = @Many(select = "com.yanc.mapper.RoleMapper.findByUid"))
    })
    SysUser findByName(String username);
}
  1. 编写RoleMapper(根据用户的id查找该用户具有的角色权限)
public interface RoleMapper {
    @Select("SELECT r.id, r.role_name roleName, r.role_desc roleDesc "+
            "FROM sys_role r ,sys_user_role ur "+
            "WHERE r.id=ur.rid AND ur.uid=#{uid}")
    public List<SysRole> findByUid(Integer uid);
}
  1. 编写服务端service层的UserService
public interface UserService extends UserDetailsService {
}
  1. UserService的实现类,若找不到用户则抛出异常
@Service
@Transactional//添加事务管理
public class UserServiceImpl implements UserService {

    @Autowired(required = false)
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        return userMapper.findByName(s);
    }
}
  1. 在资源端controller层编写ProductController.java测试授权是否成功
@RestController
@RequestMapping("/product")
public class ProductController {
    @RequestMapping("/findAll")
    public String findAll(){
        return "授权成功";
    }
}

4.授权码模式测试

  1. 首先通过服务端请求授权码,我这里的服务端的端口是9001,那么通过访问

http://localhost:9001/oauth/authorize?response_type=code&client_id=yanc

response_type参数表示要求返回授权码,client_id指定当前请求的用户

springsecurity+oauth2.0实现授权码模式_第7张图片

  1. 访问后会要求登录,下面我们提交用户信息到表单中,成功后会跳转到授权页面,可以设置read,write权限

springsecurity+oauth2.0实现授权码模式_第8张图片

  1. 授权后会回调一个地址,URL中code参数后就是授权码

在这里插入图片描述
4. 我们拿到授权码后,就可以在postman申请token了

springsecurity+oauth2.0实现授权码模式_第9张图片

点击send后,成功会返回一段json数据,access_token就是我们所需的令牌

  1. 我们携带token令牌去访问服务资源端,这里的资源端口是9002

springsecurity+oauth2.0实现授权码模式_第10张图片

你可能感兴趣的:(Java,java,安全)