实战电商后端系统(二)—— 将OAuth2认证和权限信息存入MySQL数据库

前言

本系统技术栈用到了Dubbo、Zookeeper、SpringBoot、Oauth2、Swagger、Nginx,项目刚开始起步,每完成一个大功能都会专门写一篇博文来记录技术细节以及遇到的技术难点,如项目中有哪些设计或者架构不太正确的地方,请大家在留言区中提出,互相学习~

上一章中,已经搭建好了项目的基础环境,则本文章则将OAuth2和MySQL集成起来,方便在认证和授权时能从MySQL数据库中读取client信息以及权限信息。同时,还新添加了一种授权模式:Authorization_code。

正文

一、关键表结构

表oauth_client_details

CREATE TABLE `oauth_client_details` (
  `client_id` varchar(128) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

需要注意的是,这里client_secret是要存加密过后的值。

表oauth_client_token

CREATE TABLE `oauth_client_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication_id` varchar(128) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

表tb_user

CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(64) NOT NULL COMMENT '密码,加密存储',
  `phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
  `email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`) USING BTREE,
  UNIQUE KEY `phone` (`phone`) USING BTREE,
  UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户表';

表tb_role

CREATE TABLE `tb_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',
  `name` varchar(64) NOT NULL COMMENT '角色名称',
  `enname` varchar(64) NOT NULL COMMENT '角色英文名称',
  `description` varchar(200) DEFAULT NULL COMMENT '备注',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表';

表tb_permission

CREATE TABLE `tb_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父权限',
  `name` varchar(64) NOT NULL COMMENT '权限名称',
  `enname` varchar(64) NOT NULL COMMENT '权限英文名称',
  `url` varchar(255) NOT NULL COMMENT '授权路径',
  `description` varchar(200) DEFAULT NULL COMMENT '备注',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8 COMMENT='权限表';

表tb_role_permission

CREATE TABLE `tb_role_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
  `permission_id` bigint(20) NOT NULL COMMENT '权限 ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8 COMMENT='角色权限表';

二、集成MySQL来获取授权信息

2.1 添加数据相关驱动以及插件依赖

添加数据库驱动以及tk mybatis插件

		<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.1.5</version>
        </dependency>

添加HikariCP连接池

        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <exclusions>
                <!-- 排除 tomcat-jdbc 以使用 HikariCP -->
                <exclusion>
                    <groupId>org.apache.tomcat</groupId>
                    <artifactId>tomcat-jdbc</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

三、配置OAuth2认证服务器相关配置

3.1 配置认证服务器相关配置
    @Bean
    public ClientDetailsService jdbcClientDetailsService() {
        // 基于JDBC实现,需要实现在数据库配置客户端信息以及密码加密方式
        JdbcClientDetailsService detailsService = new JdbcClientDetailsService(dataSource);
        detailsService.setPasswordEncoder(passwordEncoder);
        return detailsService;
    }
  

这里的ClientDetailsService是OAuth2用于读取认证信息的服务,默认用的是InMemoryClientDetailsService,即将客户端信息存储到内存中。

声明好了ClientDetailsService之后,就要将其配置进OAuth2中替换InMemoryClientDetailsService。

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 从数据库中读取客户端配置
        clients.withClientDetails(jdbcClientDetailsService());
	}

这样,认证相关的配置就就配完了,完整配置如下:

AuthorizationServerConfigurerAdapter

package com.bruis.oauth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import javax.sql.DataSource;
import java.util.UUID;


/**
 * @author LuoHaiYang
 *
 * 开启认证服务器
 *
 */
@Configuration
@EnableAuthorizationServer
// 开启权限控制
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    /**
     * 设置jwt加密key
     */
    private static final String JWT_SIGNING_KEY = "jwt_MC43A6m0Xt9jUIV";

    /**
     * 认证方式
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 自定义用户服务
     */
    @Autowired
    private UserDetailsService userDetailsService;
	
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Autowired
    private DataSource dataSource;

    /**
     * 配置客户端对应授权方式及客户端密码
     * 当前使用内存模式
     *
     * withClient + secret需要进行base64为加密:
     *
     * 明文:bruis:123456    BASE64:xxx
     *
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 从数据库中读取客户端配置
        clients.withClientDetails(jdbcClientDetailsService());
		/*
        clients.inMemory()
                .withClient("bruis")
                .secret(passwordEncoder.encode("123456"))
                .authorizedGrantTypes("authorization_code","password", "refresh_token")
                .scopes("all")
                .redirectUris("http://www.baidu.com");
				*/
                // 关闭授权确认步骤
                //.autoApprove(false);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore())
                // 配置密码模式需要制定认证器
                .authenticationManager(authenticationManager)
                .accessTokenConverter(accessTokenConverter())
                // 支持GET  POST  请求获取token
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                // 必须注入userDetailsService否则根据refresh_token无法加载用户信息
                .userDetailsService(userDetailsService)
                // .exceptionTranslator(customWebResponseExceptionTranslator)
                // 开启刷新token
                .reuseRefreshTokens(true);
    }

    /**
     * 认证服务器的安全配置
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")
                //isAuthenticated():排除anonymous   isFullyAuthenticated():排除anonymous以及remember-me
                .checkTokenAccess("isAuthenticated()")
                //允许表单认证
                .allowFormAuthenticationForClients();
    }


    /**
     * jwt令牌增强,添加加密key
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        // 使用OAuth2默认的JwtAccessTokenConverter
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // 密钥加强
        converter.setSigningKey(JWT_SIGNING_KEY);
        return converter;
    }

    /**
     * 使用JWT存储令牌信息
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        // 解决每次生成的 token都一样的问题
        redisTokenStore.setAuthenticationKeyGenerator(oAuth2Authentication -> UUID.randomUUID().toString());
        return redisTokenStore;
    }

    /**
     * token认证服务
     */
    @Bean
    public ResourceServerTokenServices tokenService() {
        // 授权服务和资源服务在统一项目内,可以使用本地认证方式,如果再不同工程,需要使用远程认证方式
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }

    @Bean
    public ClientDetailsService jdbcClientDetailsService() {
        // 基于JDBC实现,需要实现在数据库配置客户端信息以及密码加密方式
        JdbcClientDetailsService detailsService = new JdbcClientDetailsService(dataSource);
        detailsService.setPasswordEncoder(passwordEncoder);
        return detailsService;
    }

}
3.2 配置WebSecurityConfigurerAdapter

对于WebSecurityConfigurerAdapter的配置,这里需要申明数据源为HikariCP即可。

完整配置如下

WebSecurityConfig

package com.bruis.oauth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.sql.DataSource;


/**
 * @author LuoHaiYang
 */
@Order(2)
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 自定义用户服务类、用于用户名、密码校验、权限授权
     */
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    /**
     * 将 check_token 暴露出去,否则资源服务器访问时报403错误
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/oauth/check_token");
    }

    /**
     * 密码加密策略
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 将认证管理器注入SpringIOC容器中,用于密码模式
     * @return
     * @throws Exception
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * @Primary表示的是dataSource的bean采用DataSourceBuilder.create().build()返回的实例。
     * @return
     */
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        // 配置数据源(注意,我使用的是 HikariCP 连接池),以上注解是指定数据源,否则会有冲突
        return DataSourceBuilder.create().build();
    }
}

四、配置资源服务器

4.1 添加autoconfigure依赖

引入spring-security-oauth2-autoconfigure,用于资源服务器的自动配置

        <!-- 用于资源服务器的自动配置 -->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.2.RELEASE</version>
            <scope>compile</scope>
        </dependency>

引入这个autoconfigure的作用是在配置文件中,定义client、secret、authorize以及access_token的uri时,能够配置到OAuth2资源服务器配置类对象中。

对于本系统,gateway网关服务是作为一个资源对外开放的,所以需要引入autoconfigure,然后在配置文件中配置client、secret等配置。

在gateway服务中的application.properties文件中,添加如下配置:

# oauth2
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=secret
security.oauth2.client.access-token-uri=http://localhost:8902/oauth/token
security.oauth2.client.user-authorization-uri=http://localhost:8902/oauth/authorize
security.oauth2.resource.token-info-uri=http://localhost:8902/oauth/check_token
4.2 添加权限控制

@PreAuthorize

添加权限控制有两种方式,一种是在controller层使用注解:@PreAuthorize(“hasAuthority(‘SystemOrder’)”) 定义访问权限。

值得注意的是,@PreAuthorize可以修饰controller类以及controller方法。

在ResourceServerConfigure配置类中添加权限控制

@Override
    public void configure(HttpSecurity http) throws Exception {
        // 警用csrf,使用跨域
        http.csrf().disable()            
                .exceptionHandling()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/order/**").hasAuthority("SystemContent")                
                .antMatchers("/webjars/**", "/resources/**", "/swagger-ui.html"
                        , "/swagger-resources/**", "/v2/api-docs", "index.html").permitAll()
                .anyRequest().authenticated();
    }

五、效果展示

5.1 授权码模式

通过授权码模式去获取token,然后访问受保护的资源。

先看下oauth_client_details表的记录,client_id为client,client_secret为secret(数据库中以加密形式存储),scope为app,authorized_grant_types为authorization_code和password两种模式,而回调地址为 http://www.baidu.com。
在这里插入图片描述
通过访问:

http://localhost:8902/oauth/authorize?client_id=client&response_type=code

会首先跳转到OAuth2默认的login页面进行身份认证。

在这里插入图片描述
输入用户密码(admin/123456),就会跳转回 /oauth/authorize页面
在这里插入图片描述
这个页面也是OAuth2默认的授权页面(可以自定义,后面空了会写一个关于SpringSecurity和OAuth2底层原理的专栏,然后会介绍如何自定义OAuth2默认的login页面以及Approval页面)。

点击Approve进行确认授权,等页面重定向到百度首页后,会发现url中包含了一个code的值:VLgtje,这样我们就可以拿着这个code值去进行token的获取。
在这里插入图片描述
这里需要注意一下,对于/oauth/token的调用,需要传入 client_id和client_secret,在OAuth2中有两种方式传入,一种是以headers的形式传入经过加密的client_id和client_secret;另外一种方式就是在URL中以 client_id:client_secret的方式传入,具体示例如下:

以headers的方式传入

在这里插入图片描述
请求结果如下:
在这里插入图片描述

以 client_id:client_secret@的方式在URL中传入

由于code用过一次就不能再用了,所以要重新授权一次来演示。
重新请求的code如下:
在这里插入图片描述
效果如下:
在这里插入图片描述

5.2 密码模式

通过密码模式去获取tokne,然后访问受保护的资源。

在这里插入图片描述
密码模式的client_id和clinet_secret两种方式都可以,这里就不再赘述了。

5.3 访问受保护的资源服务

不管是通过密码模式还是授权码模式,最终的目的都是为了拿token,拿到token之后就有资格去访问受保护的资源服务了。
在本系统中,gateway就相当于一个资源服务,下面来尝试访问gateway暴露的接口。

在这里插入图片描述
在这里插入图片描述
然后再访问当前用户
在这里插入图片描述
可见,用户认证信息就是从数据库中读出的数据。

搞定…

下面贴上github源码地址,本篇文章对应的项目是在 v0.0.3分支上,数据库脚本以及nginx配置文件在项目的others目录中,有需要的读者在切换到该分支上进行操作即可。觉得博主写的不错,关注、点赞、star三连。。。

https://github.com/coderbruis/Distributed-mall

相关文章

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值