【SpringSecurity】十七、OAuth2授权服务器 + 资源服务器Demo

服务器 0

文章目录

  • 0、库表准备
  • 1、项目结构
  • 2、基于数据库的认证
  • 3、授权服务器配置
  • 4、授权服务器效果测试
  • 5、资源服务器配置
  • 6、其他授权模式测试
    • 6.1 密码模式
    • 6.2 简化模式
    • 6.3 客户端模式
    • 6.4 refresh_token模式
  • 7、令牌换为jwt格式

相关📕:【Spring Security Oauth2 配置理论部分】

0、库表准备

库表结构:

在这里插入图片描述

oauth2的相关表SQL:

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

基于RBAC,简化下,只要角色,不要权限表,表结构为:

1)用户表sys_user

在这里插入图片描述

2)角色表sys_role

在这里插入图片描述

3)用户角色关系表sys_user_role

在这里插入图片描述

1、项目结构

创建两个服务,一个充当授权服务器,结构为:

在这里插入图片描述

另一个充当资源服务器,结构为:

在这里插入图片描述

数据库层采用mysql + mybatis-plus实现,相关依赖:

<dependencies>   <!--spring security starter-->    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-security</artifactId>    </dependency>    <!--spring security oauth核心依赖-->    <dependency>        <groupId>org.springframework.security.oauth</groupId>        <artifactId>spring-security-oauth2</artifactId>        <version>2.3.4.RELEASE</version>    </dependency>    <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>    <!--mysql-->    <dependency>        <groupId>mysql</groupId>        <artifactId>mysql-connector-java</artifactId>        <version>8.0.30</version>    </dependency>    <!--mybatis-plus-->    <dependency>        <groupId>com.baomidou</groupId>        <artifactId>mybatis-plus-boot-starter</artifactId>        <version>3.4.0</version>    </dependency>    <dependency>        <groupId>com.baomidou</groupId>        <artifactId>mybatis-plus</artifactId>        <version>3.4.0</version>    </dependency>    <!--lombok-->    <dependency>        <groupId>org.projectlombok</groupId>        <artifactId>lombok</artifactId>    </dependency></dependencies>

application.yml内容:

# 资源服务器同配置,端口为9010server:  port: 9009   spring:  datasource:    driver-class-name: com.mysql.cj.jdbc.Driver    url: jdbc:mysql://localhost:3306/test-db?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=false    username: root    password: root123  main:    allow-bean-definition-overriding: truelogging:  level:    com.itheima: debugmybatis-plus:  configuration:    map-underscore-to-camel-case: true  type-aliases-package: com.plat.domain

2、基于数据库的认证

创建Po:

@TableName("sys_user")@Datapublic class SysUserPo implements Serializable {    private Integer id;    private String username;    private String password;    public Integer getId() {        return id;    }    public String getUsername() {        return username;    }    public String getPassword() {        return password;    }}
@TableName("sys_role")@Datapublic class SysRolePo implements GrantedAuthority, Serializable {    private Integer id;    private String roleName;    private String roleDesc;    @Override    public String getAuthority() {        return this.roleName;    //注意这里权限的处理,通过实现GrantedAuthority, 和框架接轨    }}

创建一个中转类,实现UserDetails,以后返回给框架(也可以用框架自己的User类,我觉得自己写个中转类更顺手)。注意其聚合SysUserPo以及权限属性。因SysUser我设计的简略,因此UserDetails的是否被禁用、是否过期等字段直接返回true,不再去自定义的SysUser中去查

@Data@Builderpublic class SecurityUser implements UserDetails {    private SysUserPo sysUserPo;    private List<SysRolePo> roles;    public SecurityUser(SysUserPo sysUserPo, List<SysRolePo> roles) {        this.sysUserPo = sysUserPo;        this.roles = roles;    }    @Override    public Collection<? extends GrantedAuthority> getAuthorities() {        return roles;    }    @Override    public String getPassword() {        return this.sysUserPo.getPassword();    }    @Override    public String getUsername() {        return this.sysUserPo.getUsername();    }    /**     * 以下字段,我的用户表设计简单,没有过期、禁用等字段     * 这里都返回true     */    @Override    public boolean isAccountNonExpired() {        return true;    }    @Override    public boolean isAccountNonLocked() {        return true;    }    @Override    public boolean isCredentialsNonExpired() {        return true;    }    @Override    public boolean isEnabled() {        return true;    }}

Mapper:

@Repository@Mapperpublic interface UserMapper extends BaseMapper<SysUserPo> {    @Select("select * from sys_user where username = #{username}")    SysUserPo selectUserByName(String username);}
@Repository@Mapperpublic interface RoleMapper extends BaseMapper<SysRolePo> {    @Select("SELECT r.id, r.role_name roleName, r.role_desc roleDesc "+            "FROM sys_role r ,sys_user_role ur "+            "WHERE r.id=ur.role_id AND ur.user_id=#{uid}")    public List<SysRolePo> selectAuthByUserId(Integer uid);}

写UserDetialsService接口的实现类,好自定义用户查询逻辑:

public interface UserService extends UserDetailsService {
@Servicepublic class UserServiceImpl implements UserService {    @Resource    private UserMapper userMapper;    @Resource    private RoleMapper roleMapper;    @Override    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {        //自定义用户类        SysUserPo sysUserPo = userMapper.selectUserByName(username);        //权限        List<SysRolePo> authList = roleMapper.selectAuthByUserId(sysUserPo.getId());        return new SecurityUser(sysUserPo, authList);    }}

3、授权服务器配置

注入DataSource对象,定义授权服务器需要的相关Bean:

@Configurationpublic class OAuth2Bean {    @Resource    private DataSource dataSource;  //数据库连接池对象    /**     * 客户端服务详情     * 从数据库查询客户端信息     */    @Bean(name = "jdbcClientDetailsService")    public JdbcClientDetailsService clientDetailsService(){        return new JdbcClientDetailsService(dataSource);    }    /**     * 授权信息保存策略     */    @Bean(name = "jdbcApprovalStore")    public ApprovalStore approvalStore(){        return new JdbcApprovalStore(dataSource);    }    /**     * 令牌存储策略     */    @Bean(name = "jdbcTokenStore")    public TokenStore tokenStore(){        //使用数据库存储令牌        return new JdbcTokenStore(dataSource);    }    //设置授权码模式下,授权码如何存储    @Bean(name = "jdbcAuthorizationCodeServices")    public AuthorizationCodeServices authorizationCodeServices(){        return new JdbcAuthorizationCodeServices(dataSource);    }}

配置OAuth2的授权服务器:

@Configuration@EnableAuthorizationServer    //OAuth2的授权服务器public class OAuth2ServiceConfig implements AuthorizationServerConfigurer {    @Resource(name = "jdbcTokenStore")    private TokenStore tokenStore;    //注入自定义的token存储配置Bean    @Resource(name = "jdbcClientDetailsService")    private ClientDetailsService clientDetailsService;  //客户端角色详情    @Resource    private AuthenticationManager authenticationManager;  //注入安全配置类中定义的认证管理器Bean    @Resource(name = "jdbcAuthorizationCodeServices")    private AuthorizationCodeServices authorizationCodeServices;  //注入自定义的授权码模式服务配置Bean    @Resource(name = "jdbcApprovalStore")    private ApprovalStore approvalStore;   //授权信息保存策略    //token令牌管理    @Bean    public AuthorizationServerTokenServices tokenServices() {        DefaultTokenServices tokenServices = new DefaultTokenServices();        tokenServices.setClientDetailsService(clientDetailsService);   //客户端信息服务,即向哪个客户端颁发令牌        tokenServices.setSupportRefreshToken(true);  //支持产生刷新令牌        tokenServices.setTokenStore(tokenStore);   //令牌的存储策略        tokenServices.setAccessTokenValiditySeconds(7200);    //令牌默认有效期2小时        tokenServices.setRefreshTokenValiditySeconds(259200);  //refresh_token默认有效期三天        return tokenServices;    }    /**     * token令牌端点访问的安全策略     * (不是所有人都可以来访问框架提供的这些令牌端点的)     */    @Override    public void configure(AuthorizationServerSecurityConfigurer authorizationServerSecurityConfigurer) throws Exception {        authorizationServerSecurityConfigurer.tokenKeyAccess("permitAll()")   //oauth/token_key这个端点(url)是公开的,不用登录可调                .checkTokenAccess("permitAll()")   // oauth/check_token这个端点是公开的                .allowFormAuthenticationForClients();  //允许客户端表单认证,申请令牌    }    /**     * Oauth2.0客户端角色的信息来源:内存、数据库     * 这里用数据库     */    @Override    public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {        clientDetailsServiceConfigurer.withClientDetails(clientDetailsService);    }    /**     * 令牌端点访问和令牌服务(令牌怎么生成、怎么存储等)     */    @Override    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {        endpoints.authenticationManager(authenticationManager)  //设置认证管理器,密码模式需要                .authorizationCodeServices(authorizationCodeServices)  //授权码模式需要                .approvalStore(approvalStore)                .tokenServices(tokenServices())  //token管理服务                .allowedTokenEndpointRequestMethods(HttpMethod.POST);  //允许Post方式访问    }}

web安全配置类:

@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    @Resource    private UserService userService;    //设置权限    @Override    protected 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();    }    //指定认证对象的来源    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());    }    @Bean    public PasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }}

授权服务器配置完成,启动服务。

4、授权服务器效果测试

浏览器模拟客户端系统请求资源,客户端系统自已重定向到以下路径:

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

向服务方获取授权码。到达服务方系统的登录页面,输入用户在服务方系统的账户密码:

在这里插入图片描述

服务方系统校验通过,询问用户是否向c1客户端系统开放权限all去获取它的资源:

在这里插入图片描述
点击同意,重定向到客户端注册的redirect_url,并返回授权码:

在这里插入图片描述

客户端系统用授权码去/oauth/token换取令牌:

在这里插入图片描述

成功获得令牌。携带此令牌向资源服务器发起请求。

ps:复习认证授权的对接流程

在这里插入图片描述

  • 客户端系统向本地服务发起授权申请
  • 客户端系统授权地址重定向到服务端系统的/oauth/authorize接口
  • 客户端系统向服务端系统的认证中心发起授权申请
  • 服务端系统校验是否已登录
  • 未登录则需要在服务端系统页面完成用户登录
  • 服务端系统认证中心发放授权码
  • 客户端系统申请token
  • 客户端系统使用code向服务端换取token
  • 服务端系统返回token及有效期
  • 服务端系统同步缓存token
  • 返回token给客户端系统

5、资源服务器配置

配置一个远程校验token的Bean,设置校验token的端点url,以及资源服务自己的客户端id和密钥:

@Configurationpublic class BeanConfig {    @Bean    public ResourceServerTokenServices tokenServices() {        RemoteTokenServices services = new RemoteTokenServices();        services.setCheckTokenEndpointUrl("http://localhost:9009/oauth/check_token");        services.setClientId("resourceServiceId");        services.setClientSecret("123");        return services;    }}

配置授权服务器:

@Configuration@EnableResourceServer@EnableGlobalMethodSecurity(securedEnabled = true)public class OAuthSourceConfig extends ResourceServerConfigurerAdapter {    public static final String RESOURCE_ID = "res1";    @Resource    private DataSource dataSource;    @Resource    ResourceServerTokenServices resourceServerTokenServices;    @Bean    public TokenStore jdbcTokenStore() {        return new JdbcTokenStore(dataSource);    }    @Override    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {        resources.resourceId(RESOURCE_ID)   //资源id                .tokenStore(jdbcTokenStore())   //告诉资源服务token在库里                .tokenServices(resourceServerTokenServices)                .stateless(true);    }    @Override    public void configure(HttpSecurity http) throws Exception {        http.authorizeRequests()                //这就是给客户端发token时的scope,这里会校验scope标识                .antMatchers("/**").access("#oauth2.hasAnyScope('all')")                .and()                .csrf().disable()                .sessionManagement()                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);    }}

写个测试接口:

@RestControllerpublic class ResourceController {    @GetMapping("/r/r1")    public String r1(){        return "access resource 1";    }    @GetMapping("/r/r2")    public String r2(){        return "access resource 2";    }}

携带上面申请的令牌访问测试接口。token正确时:

在这里插入图片描述

token错误时:

在这里插入图片描述

6、其他授权模式测试

上面测完了授权码模式,该模式最安全,因为access_token只在服务端在交换,而不经过浏览器,令牌不容易泄露。

6.1 密码模式

测试密码模式,刚开始报错:unauthorized grant type:password。

在这里插入图片描述

想起客户端注册信息是我手动插入到oauth表里的,新改个字段:

在这里插入图片描述

一切正常:

在这里插入图片描述
很明显,这种模式会把用户在服务端系统的账户和密码泄漏给客户端系统。因此该模式一般用于客户端系统也是自己公司开发的情况。

6.2 简化模式

相比授权码模式,少了一步授权码换token的步骤。

在这里插入图片描述
response_type=token,说明是简化模式。

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

在这里插入图片描述

简化模式用于客户端只是个前端页面的情况。即没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码+换取token

6.3 客户端模式

使用客户端模式:

在这里插入图片描述

/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials参数:- client_id:客户端准入标识。- client_secret:客户端秘钥。- grant_type:授权类型,填写client_credentials表示客户端模式

简单但不安全,需要对客户端系统很信任,可用于合作方系统间对接:

在这里插入图片描述

6.4 refresh_token模式

在这里插入图片描述

7、令牌换为jwt格式

以上Demo,资源服务校验令牌的合法性得通过RemoteTokenServices来调用授权服务的/oauth/check_token接口。如此,会影响系统的性能。 ⇒ 引入JWT 。让资源服务不再需要远程调用授权服务来校验令牌,而是让资源服务本身就可以校验。相关依赖:

<dependency>    <groupId>org.springframework.security</groupId>    <artifactId>spring-security-jwt</artifactId>    <version>1.0.9.RELEASE</version></dependency>

授权服务改动:设置对称密钥,令牌存储策略TokenStore改为JwtTokenStore

@Configurationpublic class OAuth2Bean {    @Value("${jwt.secret:oauth9527}")    private String SIGNING_KEY;    @Resource    private DataSource dataSource;  //数据库连接池对象    @Resource(name = "bCryptPasswordEncoder")    private PasswordEncoder passwordEncoder;	 /**     * 令牌存储策略     */    @Bean(name = "jwtTokenStore")    public TokenStore tokenStore(){        //JWT        return new JwtTokenStore(accessTokenConverter());    }    @Bean    public JwtAccessTokenConverter accessTokenConverter(){        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();        converter.setSigningKey(SIGNING_KEY);  //对称秘钥,资源服务器使用该秘钥来验证        return converter;    }    /**     * 客户端服务详情     * 从数据库查询客户端信息     */    @Bean(name = "jdbcClientDetailsService")    public JdbcClientDetailsService clientDetailsService(){        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);        jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);        return jdbcClientDetailsService;    }    /**     * 授权信息保存策略     */    @Bean(name = "jdbcApprovalStore")    public ApprovalStore approvalStore(){        return new JdbcApprovalStore(dataSource);    }        //设置授权码模式下,授权码如何存储    @Bean(name = "jdbcAuthorizationCodeServices")    public AuthorizationCodeServices authorizationCodeServices(){        return new JdbcAuthorizationCodeServices(dataSource);    }}

TokenService新增后面的令牌增强:

//token令牌管理@Beanpublic AuthorizationServerTokenServices tokenServices() {    DefaultTokenServices tokenServices = new DefaultTokenServices();    tokenServices.setClientDetailsService(clientDetailsService);   //客户端信息服务,即向哪个客户端颁发令牌    tokenServices.setSupportRefreshToken(true);  //支持产生刷新令牌    tokenServices.setTokenStore(tokenStore);   //令牌的存储策略    tokenServices.setAccessTokenValiditySeconds(7200);    //令牌默认有效期2小时    tokenServices.setRefreshTokenValiditySeconds(259200);  //refresh_token默认有效期三天    //令牌增强    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();    tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));    tokenServices.setTokenEnhancer(tokenEnhancerChain);    return tokenServices;}

资源服务器上,使用同一个对称密钥以及JwtTokenStore:

@Configurationpublic class BeanConfig {    @Value("${jwt.secret:oauth9527}")    private String SIGNING_KEY;    /**     * 令牌存储策略     */    @Bean(name = "jwtTokenStore")    public TokenStore tokenStore(){        //JWT        return new JwtTokenStore(accessTokenConverter());    }    @Bean    public JwtAccessTokenConverter accessTokenConverter(){        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();        converter.setSigningKey(SIGNING_KEY);  //对称秘钥,资源服务器使用该秘钥来验证        return converter;    }	//@Bean   //不再需要这个远程校验token的Bean了    public ResourceServerTokenServices tokenServices() {        RemoteTokenServices services = new RemoteTokenServices();        services.setCheckTokenEndpointUrl("http://localhost:9009/oauth/check_token");        services.setClientId("resourceServiceId");        services.setClientSecret("123");        return services;    }}

资源服务器配置类中,不再需要远程校验的RemoteTokenServices

在这里插入图片描述

验证下效果:

在这里插入图片描述

携带jwt的token访问资源服务:

在这里插入图片描述

也许您对下面的内容还感兴趣: