Spring Secuirty Oauth2.0(RABC版本配置)

SpringBoot单体服务版配置 Oauth2.0

  • 数据库:PostgresSQL
  • 开发工具:idea
  • 开发框架:SpringBoot
  • 数据库操作:MyBatisPlus
  • 实体类工具:lombok

数据库配置

RBAC 是基于角色的访问控制(Role-Based Access Control )在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。

image-20210527230833093

用户表

create table auth_user
(
    uuid         uuid        default uuid_generate_v4() not null
        constraint auth_user_pkey
            primary key,
    id           bigserial                              not null,
    username     varchar(128)                           not null,
    password     varchar(128)                           not null,
    phone_number varchar(128),
    email        varchar(128),
    is_deleted   boolean     default false,
    is_enable    boolean     default true,
    is_show      boolean     default true,
    revision     integer     default 0,
    created_by   varchar(32) default 'bztdt_admin'::character varying,
    created_time date        default now(),
    updated_by   varchar(32) default 'bztdt_admin'::character varying,
    updated_time date        default now()
);

comment on table auth_user is '用户信息表';

comment on column auth_user.uuid is '唯一ID';

comment on column auth_user.id is '自增id';

comment on column auth_user.username is '不能重复,不能过长';

comment on column auth_user.password is '进行加密';

comment on column auth_user.phone_number is '13位';

comment on column auth_user.email is '符合规范';

comment on column auth_user.is_deleted is '删除时,将字段设置为true';

comment on column auth_user.is_enable is '是否启用账号';

comment on column auth_user.is_show is '管理员账号不显示';

comment on column auth_user.revision is '默认为0,每次修改加一';

comment on column auth_user.created_by is '默认为admin';

comment on column auth_user.created_time is '创建时间';

comment on column auth_user.updated_by is '默认为admin';

comment on column auth_user.updated_time is '更新时间';

alter table auth_user
    owner to postgres;

角色表

create table auth_role
(
    id           bigserial    not null
        constraint auth_role_pkey
            primary key,
    parent_id    bigint,
    name         varchar(128) not null,
    enname       varchar(128) not null,
    description  varchar(512),
    is_deleted   boolean     default false,
    sort_code    varchar(32),
    revision     integer     default 0,
    created_by   varchar(32) default 'bztdt_admin'::character varying,
    created_time date        default now(),
    updated_by   varchar(32) default 'bztdt_admin'::character varying,
    updated_time date        default now()
);

comment on table auth_role is '角色表';

comment on column auth_role.id is '唯一值';

comment on column auth_role.parent_id is '父角色';

comment on column auth_role.name is '角色名称';

comment on column auth_role.enname is '角色英文名称';

comment on column auth_role.description is '备注';

comment on column auth_role.is_deleted is '是否删除';

comment on column auth_role.sort_code is '排序代码';

comment on column auth_role.revision is '乐观锁';

comment on column auth_role.created_by is '创建人';

comment on column auth_role.created_time is '创建时间';

comment on column auth_role.updated_by is '更新人';

comment on column auth_role.updated_time is '更新时间';

alter table auth_role
    owner to postgres;

权限表

create table auth_permission
(
    id           bigserial                                          not null
        constraint auth_permission_pkey
            primary key,
    parent_id    bigint,
    name         varchar(128)                                       not null,
    enname       varchar(128)                                       not null,
    url          varchar(1024)                                      not null,
    resource_id  varchar(32) default 'resources'::character varying not null,
    is_deleted   boolean     default false,
    sort_code    varchar(32),
    revision     integer     default 0,
    created_by   varchar(32) default 'bztdt_admin'::character varying,
    created_time date        default now(),
    updated_by   varchar(32) default 'bztdt_admin'::character varying,
    updated_time date        default now()
);

comment on table auth_permission is '权限表,后台接口访问权限管理';

comment on column auth_permission.id is '唯一id';

comment on column auth_permission.parent_id is '父权限';

comment on column auth_permission.name is '权限名称';

comment on column auth_permission.enname is '权限英文名称';

comment on column auth_permission.url is '授权路径';

comment on column auth_permission.resource_id is '资源服务器id';

comment on column auth_permission.is_deleted is '是否删除';

comment on column auth_permission.sort_code is '排序代码';

comment on column auth_permission.revision is '乐观锁';

comment on column auth_permission.created_by is '创建人';

comment on column auth_permission.created_time is '创建时间';

comment on column auth_permission.updated_by is '更新人';

comment on column auth_permission.updated_time is '更新时间';

alter table auth_permission
    owner to postgres;

用户角色表

create table auth_user_role
(
    id      bigserial not null
        constraint auth_user_role_pkey
            primary key,
    role_id bigint    not null,
    user_id uuid
);

comment on table auth_user_role is '用户角色表';

comment on column auth_user_role.id is '唯一id';

comment on column auth_user_role.role_id is '角色id';

alter table auth_user_role
    owner to postgres;

角色权限表

create table auth_role_permission
(
    id            bigserial not null
        constraint auth_role_permission_pkey
            primary key,
    role_id       bigint    not null,
    permission_id bigint    not null
);

comment on table auth_role_permission is '角色权限表';

comment on column auth_role_permission.id is '唯一id';

comment on column auth_role_permission.role_id is '角色ID';

comment on column auth_role_permission.permission_id is '权限ID';

alter table auth_role_permission
    owner to postgres;

代码

项目结构

image-20210527214659640

  • 启用密码模式
  • 使用Redis存储令牌
  • 单体项目认证服务器资源服务器在同一个项目中

pom文件

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.sun</groupId>
    <artifactId>test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>test</name>
    <description>test</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </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>
        <!-- Spring Cloud -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
        <!--数据库相关-->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <!--    redis    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--tools-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>



    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml文件

server:
  port: 8888
  url: localhost
spring:
  servlet:
    multipart:
      max-file-size: -1MB
      max-request-size: -1MB
  mvc:
    async:
      request-timeout: 20000
  devtools:
    restart:
      enabled: false
      additional-paths: src/main/java
      exclude: WEB-INF/**
  datasource:
    url: jdbc:postgresql://${
    
    base.config.db.hostname}:${
    
    base.config.db.port}/${
    
    base.config.db.db}?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: ${
    
    base.config.db.username}
    password: ${
    
    base.config.db.password}
    driver-class-name: org.postgresql.Driver
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 5
      idle-timeout: 600000
      maximum-pool-size: 10
      auto-commit: true
      pool-name: MyHikariCP
      max-lifetime: 1800000
      connection-timeout: 30000
      connection-test-query: SELECT 1
  redis:
    port: ${
    
    base.config.redis.port}
    host: ${
    
    base.config.redis.hostname}
    password: ${
    
    base.config.redis.password}
    database: 0
    timeout: 10000
    jedis:
      pool:
        max-active: 200
        max-wait: -1
        max-idle: 10
        min-idle: 0


mybatis:
  mapper-locations: classpath*:mapper/*.xml
  type-aliases-package: com.sun.test.entity
  configuration:
    map-underscore-to-camel-case: true

security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
      access-token-uri: http://${
    
    server.url}:${
    
    server.port}/oauth/token
      user-authorization-uri: http://${
    
    server.url}:${
    
    server.port}/oauth/authorize
    resource:
      token-info-uri: http://${
    
    server.url}:${
    
    server.port}/oauth/check_token
    authorization:
      check-token-access: http://${
    
    server.url}:${
    
    server.port}/oauth/check_token

oauth2:
  grant_type: password
  client_id: client
  client_secret: secret

logging:
  level:
    com.zykj.bztdt.mapper: debug

base:
  config:
    db:
      hostname: 192.168.1.111
      port: 5432
      db: test
      username: postgres
      password: postgres
    redis:
      hostname: 192.168.1.96
      port: 6379
      password: 123456
      database: 0

实体类

用户信息实体类

  • AuthUser
import java.time.LocalDate;
import java.io.Serializable;

import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * <p>
 * 用户信息表
 * </p>
 *
 * @author sung
 * @since 2021-05-25
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class AuthUser implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 唯一ID
     */
    private String uuid;

    /**
     * 自增id
     */
    private Long id;

    /**
     * 不能重复,不能过长
     */
    private String username;

    /**
     * 进行加密
     */
    private String password;

    /**
     * 13位
     */
    private String phoneNumber;

    /**
     * 符合规范
     */
    private String email;

    /**
     * 删除时,将字段设置为true
     */
    private Boolean isDeleted;

    /**
     * 管理员账号不显示
     */
    private Boolean isShow;

    /**
     * 是否启用账号
     */
    private Boolean isEnable;

    /**
     * 默认为0,每次修改加一
     */
    private Integer revision;

    /**
     * 默认为admin
     */
    private String createdBy;

    /**
     * 创建时间
     */
    private LocalDate createdTime;

    /**
     * 默认为admin
     */
    private String updatedBy;

    /**
     * 更新时间
     */
    private LocalDate updatedTime;


}

角色实体类

  • AuthRole
import java.time.LocalDate;
import java.io.Serializable;

import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * <p>
 * 用户信息表
 * </p>
 *
 * @author sung
 * @since 2021-05-25
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class AuthUser implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    /**
     * 唯一ID
     */
    private String uuid;

    /**
     * 自增id
     */
    private Long id;

    /**
     * 不能重复,不能过长
     */
    private String username;

    /**
     * 进行加密
     */
    private String password;

    /**
     * 13位
     */
    private String phoneNumber;

    /**
     * 符合规范
     */
    private String email;

    /**
     * 删除时,将字段设置为true
     */
    private Boolean isDeleted;

    /**
     * 管理员账号不显示
     */
    private Boolean isShow;

    /**
     * 是否启用账号
     */
    private Boolean isEnable;

    /**
     * 默认为0,每次修改加一
     */
    private Integer revision;

    /**
     * 默认为admin
     */
    private String createdBy;

    /**
     * 创建时间
     */
    private LocalDate createdTime;

    /**
     * 默认为admin
     */
    private String updatedBy;

    /**
     * 更新时间
     */
    private LocalDate updatedTime;


}

权限实体类

  • AuthPermission
import java.time.LocalDate;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * <p>
 * 权限表,后台接口访问权限管理
 * </p>
 *
 * @author sung
 * @since 2021-05-25
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class AuthPermission implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    /**
     * 唯一id
     */
    private Long id;

    /**
     * 父权限
     */
    private Long parentId;

    /**
     * 权限名称
     */
    private String name;

    /**
     * 权限英文名称
     */
    private String enname;

    /**
     * 授权路径
     */
    private String url;

    /**
     * 是否删除
     */
    private Boolean isDeleted;

    /**
     * 排序代码
     */
    private String sortCode;

    /**
     * 乐观锁
     */
    private Integer revision;

    /**
     * 创建人
     */
    private String createdBy;

    /**
     * 创建时间
     */
    private LocalDate createdTime;

    /**
     * 更新人
     */
    private String updatedBy;

    /**
     * 更新时间
     */
    private LocalDate updatedTime;


}

用户角色实体类

  • AuthUserRole
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * <p>
 * 用户角色表
 * </p>
 *
 * @author sung
 * @since 2021-05-25
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class AuthUserRole implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    /**
     * 唯一id
     */
    private Long id;

    /**
     * 角色id
     */
    private Long roleId;

    /**
     * 用户id
     */
    private Long userId;


}

角色权限实体类

  • AuthRolePermission
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * <p>
 * 角色权限表
 * </p>
 *
 * @author sung
 * @since 2021-05-25
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class AuthRolePermission implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    /**
     * 唯一id
     */
    private Long id;

    /**
     * 角色ID
     */
    private Long roleId;

    /**
     * 权限ID
     */
    private Long permissionId;


}

Mapper层服务

用户信息表 Mapper 接口

import com.sun.test.entity.AuthUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
 * <p>
 * 用户信息表 Mapper 接口
 * </p>
 *
 * @author sung
 * @since 2021-05-25
 */
public interface AuthUserMapper extends BaseMapper<AuthUser> {
    
    

    /**
     * 通过用户名获取用户信息
     *
     * @param username 用户名
     * @return User
     */
    AuthUser getInfoByName(String username);

}

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">
<mapper namespace="com.sun.test.mapper.AuthUserMapper">

    <select id="getInfoByName" resultType="com.zykj.bztdt.entity.AuthUser">
        select * from auth_user where username=#{username}
    </select>
</mapper>

权限信息获取接口

import com.sun.test.entity.AuthPermission;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import java.util.List;

/**
 * <p>
 * 权限表,后台接口访问权限管理 Mapper 接口
 * </p>
 *
 * @author sung
 * @since 2021-05-25
 */
public interface AuthPermissionMapper extends BaseMapper<AuthPermission> {
    
    

    /**
     * 通过资源服务器id,获取所有的权限地址
     *
     * @param resourceId 资源服务器id
     * @return list
     */
    List<AuthPermission> getAllUrlByResourceId(String resourceId);

    /**
     * 通过用户uuid获取用户权限
     *
     * @param userId 用户id
     * @return list
     */
    List<AuthPermission> selectByUserId(String userId);

}

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">
<mapper namespace="com.sun.test.mapper.AuthPermissionMapper">

    <select id="getAllUrlByResourceId" resultType="com.sun.test.entity.AuthPermission">
        select *
        from auth_permission
        where resource_id = #{resourceId};
    </select>
    <select id="selectByUserId" resultType="com.sun.test.entity.AuthPermission">
        select p.*
        from auth_user as u
                 right join auth_user_role as ur
                            on u.uuid = ur.user_id
                 right join auth_role as r
                            on ur.role_id = r.id
                 right join auth_role_permission as rp
                            on r.id = rp.role_id
                 right join auth_permission as p
                            on rp.permission_id = p.id and p.is_deleted=false
        where u.uuid = #{userId}::uuid
    </select>
</mapper>

认证服务器配置

服务器安全配置

WebSecurityConfiguration

创建一个类继承 WebSecurityConfigurerAdapter 并添加相关注解:

  • @Configuration
  • @EnableWebSecurity
  • @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true):全局方法拦截
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;

/**
 * @author sungang
 * 认证服务器安全配置
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    
 
  //配置加密方式
  @Bean
    public BCryptPasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

  
    @Bean
    @Override
    public UserDetailsService userDetailsServiceBean() {
    
    
        return new UserDetailsServiceImpl();
    }

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

    /**
     * 用于支持password模式
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }

    String[] SWAGGER_WHITELIST = {
    
    
            "/swagger-ui.html",
            "/swagger-ui/*",
            "/swagger-resources/**",
            "/v2/api-docs",
            "/v3/api-docs",
            "/webjars/**"
    };

    @Override
    public void configure(WebSecurity web) throws Exception {
    
    
      //配置忽略的接口
        web.ignoring()
                .antMatchers("/auth/**")
                .antMatchers("/oauth/check_token")
                .antMatchers(SWAGGER_WHITELIST);
    }


}

自定义认证授权实现类

UserDetailsServiceImpl

创建一个类,实现 UserDetailsService 接口,代码如下:

扫描二维码关注公众号,回复: 13558547 查看本文章
import com.google.common.collect.Lists;
import com.zykj.bztdt.entity.AuthPermission;
import com.zykj.bztdt.entity.AuthUser;
import com.zykj.bztdt.mapper.AuthPermissionMapper;
import com.zykj.bztdt.mapper.AuthUserMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;

/**
 * @author sun
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    


    @Resource
    AuthUserMapper authUserMapper;

    @Resource
    AuthPermissionMapper authPermissionMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    
    
        AuthUser user = authUserMapper.getInfoByName(s);
        if (user != null) {
    
    
            List<GrantedAuthority> grantedAuthorities = Lists.newArrayList();
            List<AuthPermission> permissions = authPermissionMapper.selectByUserId(user.getUuid());
            for (AuthPermission p : permissions
            ) {
    
    
                SimpleGrantedAuthority roleUser = new SimpleGrantedAuthority(p.getEnname());
                grantedAuthorities.add(roleUser);
            }

            return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
        }
        return null;
    }

}

令牌有多种存储方式,每种方式都是实现了 TokenStore 接口

  • 存储在本机内存: InMemoryTokenStore
  • 存储在数据库: JdbcTokenStore
  • JWT: JwtTokenStore,Json Web Token 不会存储在任何介质中,不过我还是不推荐这种做法啊,并发 2w 以后会有问题哒,谁用谁知道额
  • 存储在 Redis: RedisTokenStore

配置认证服务器

AuthorizationServerConfiguration

创建一个类继承 AuthorizationServerConfigurerAdapter 并添加相关注解:

  • @Configuration
  • @EnableAuthorizationServer
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
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.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import javax.annotation.Resource;


/**
 * @author sungang
 * 配置认证服务器
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    
    

    @Resource
    private BCryptPasswordEncoder passwordEncoder;

    /**
     * 注入用于支持password模式
     */
    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    /**
     * Refresh Token 时需要自定义实现,否则抛异常 <br>
     * Lazy 注解是为了防止循环注入(is there an unresolvable circular reference?)
     */
    @Lazy
    @Resource(name = "userDetailsServiceBean")
    private UserDetailsService userDetailsService;

    /**
     * 注入redis工厂的bean
     */
    @Bean
    public TokenStore tokenStore() {
    
    
        return new RedisTokenStore(redisConnectionFactory);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    
    
        // 用于支持密码模式
        endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore());
        // Refresh Token 时需要自定义实现,否则抛异常
        endpoints.userDetailsService(userDetailsService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    
    
        security
                // 允许客户端访问/oauth/check_token 检查token
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }


    /**
     * 配置客户端
     *
     * @param clients 客户端连接
     * @throws Exception 异常
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        clients
                // 使用内存设置
                .inMemory()
                // client_id
                .withClient("client")
                // client_secret
                .secret(passwordEncoder.encode("secret"))
                // 授权类型,密码模式和刷新令牌
                .authorizedGrantTypes("password", "refresh_token")
                // 授权范围
                .scopes("backend")
                // 可以设置对哪些资源有访问权限,不设置则全部资源都可以访问
                .resourceIds("resources")
                // 设置访问令牌的有效期,这里是1天
                .accessTokenValiditySeconds(60 * 60 * 24)
                // 设置刷新令牌的有效期,这里是30天
                .refreshTokenValiditySeconds(60 * 60 * 24 * 30);

    }

}

资源服务器配置

ResourceServerConfiguration

创建一个类继承 ResourceServerConfigurerAdapter 并添加相关注解:

  • @Configuration
  • @EnableResourceServer:资源服务器
  • @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true):全局方法拦截
import com.zykj.bztdt.service.PermitService;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
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 javax.annotation.Resource;
import java.util.Map;

/**
 * @author sungang
 * <p>
 * 资源服务管理
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    
    

    @Resource
    private PermitService permissionService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
    
    
        // 管理员授权请求路径
      //从数据库动态获取可访问权限地址
        Map<String, String> map = permissionService.getAllUrlByResourceId("resources");
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
                expressionInterceptUrlRegistry = http.exceptionHandling()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().authorizeRequests();
        for (String key : map.keySet()) {
    
    
            expressionInterceptUrlRegistry.antMatchers(key).hasAnyAuthority(map.get(key));
        }

    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resource) throws Exception {
    
    
       //指定Token异常信息  
      resource.authenticationEntryPoint(new AuthExceptionEntryPoint()).accessDeniedHandler(new CustomAccessDeniedHandler());
      //设置资源服务器id
        resource.resourceId("resources").stateless(true);
    }
}

权限控制service

import java.util.Map;

/**
 * @author sungang
 * @date 2021/5/26 8:51 上午
 * 权限控制使用
 */
public interface PermitService {
    
    

    /**
     * 通过资源服务器id,获取所有的权限地址
     *
     * @param resourceId 资源服务器id
     * @return map
     */
    Map<String, String> getAllUrlByResourceId(String resourceId);
}

权限控制实现类

import com.sun.test.entity.AuthPermission;
import com.sun.test.mapper.AuthPermissionMapper;
import com.sun.test.service.PermitService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author sungang
 * @date 2021/5/26 8:51 上午
 */
@Service
public class PermitServiceImpl implements PermitService {
    
    

    @Resource
    AuthPermissionMapper authPermissionMapper;

    @Override
    public Map<String, String> getAllUrlByResourceId(String resourceId) {
    
    
        List<AuthPermission> allUrlByResourceId = authPermissionMapper.getAllUrlByResourceId(resourceId);
        Map<String, String> map = new HashMap<>();
        for (AuthPermission p : allUrlByResourceId
        ) {
    
    
            map.put(p.getUrl(), p.getEnname());
        }
        return map;
    }
}

这里使用到的mapper是上面的AuthPermissionMapper

配置自定义的异常处理

权限不足异常类重写

  • CustomAccessDeniedHandler
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author sungang
 * 权限不足异常类重写
 */
@Component("customAccessDeniedHandler")
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    
    
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("code", "403");
        map.put("msg", "权限不足");
        map.put("data", accessDeniedException.getMessage());
        map.put("success", false);
        map.put("path", request.getServletPath());
        map.put("timestamp", String.valueOf(System.currentTimeMillis()));
        ObjectMapper mapper = new ObjectMapper();
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write(mapper.writeValueAsString(map));
    }
}

无效token异常重写

  • AuthExceptionEntryPoint
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;


/**
 * @author sungang
 * 无效token异常重写
 */
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {
    
    

    private static final Logger log = LoggerFactory.getLogger(AuthExceptionEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    
    
        Map<String, Object> map = new HashMap<String, Object>();
        Throwable cause = authException.getCause();
        if (cause instanceof InvalidTokenException) {
    
    
            map.put("code", "401");
            map.put("msg", "无效的token");
        } else {
    
    
            map.put("code", "401");
            map.put("msg", "访问此资源需要完全的身份验证!");
        }
        map.put("data", authException.getMessage());
        map.put("success", false);
        map.put("path", request.getServletPath());
        map.put("timestamp", String.valueOf(System.currentTimeMillis()));
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        try {
    
    
            ObjectMapper mapper = new ObjectMapper();
            mapper.writeValue(response.getOutputStream(), map);
        } catch (Exception e) {
    
    
            log.error(e.getMessage());
            throw new ServletException();
        }
    }
}

Controller接口

import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.sun.test.entity.pojo.LoginPojo;
import com.sun.test.response.ResponseCode;
import com.sun.test.response.ResponseResult;
import com.sun.test.service.AuthService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.Map;

/**
 * @author sungang
 * @date 2021/5/26 1:21 下午
 * <p>
 * 用户基础权限控制
 */
@Api(tags = "01-用户基础权限控制")
@RestController
@RequestMapping(value = "/auth")
public class AuthController {
    
    

    @Resource
    AuthService authService;

    @ApiOperationSupport(order = 1)
    @ApiOperation("用户登录")
    @PostMapping(value = "login")
    public ResponseResult login(@RequestBody LoginPojo loginPojo) {
    
    
        return authService.login(loginPojo);
    }



    @ApiOperationSupport(order = 2)
    @PostMapping("logout")
    @ApiOperation("用户注销")
    public ResponseResult logout(@RequestParam("accessToken") String accessToken) {
    
    
        return authService.logout(accessToken);
    }

    @ApiOperationSupport(order = 3)
    @ApiOperation("刷新access_token")
    @ApiImplicitParams({
    
    
            @ApiImplicitParam(name = "accessToken", value = "用户token", required = true, dataType = "String")
    })
    @PostMapping(value = "refresh")
    public ResponseResult refresh(@RequestParam("accessToken") String accessToken) {
    
    
        Map<String, String> refresh = authService.refresh(accessToken);
        return new ResponseResult(ResponseCode.SUCCESS, refresh);
    }


}

Service接口

import com.sun.test.entity.pojo.LoginPojo;
import com.sun.test.response.ResponseResult;

import java.util.Map;

/**
 * @author sungang
 * @date 2021/5/26 1:27 下午
 * <p>
 * 用户基础权限控制
 */
public interface AuthService {
    
    

    /**
     * 用戶登录验证接口
     *
     * @param loginPojo 用户登录实体类
     * @return ResponseResult
     */
    ResponseResult login(LoginPojo loginPojo);

    /**
     * 用户注销
     *
     * @param accessToken token
     * @return ResponseResult
     */
    ResponseResult logout(String accessToken);

    /**
     * 刷新Token
     *
     * @param accessToken 使用旧Token换取新Token
     * @return Map
     */
    Map<String, String> refresh(String accessToken);
}

Service实现类

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.sun.test.entity.AuthUser;
import com.sun.test.entity.pojo.LoginPojo;
import com.sun.test.exception.BizException;
import com.sun.test.mapper.AuthUserMapper;
import com.sun.test.response.ResponseCode;
import com.sun.test.response.ResponseResult;
import com.sun.test.service.AuthService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author sungang
 * @date 2021/5/26 1:28 下午
 */
@Service
public class AuthServiceImpl implements AuthService {
    
    

    @Resource
    public BCryptPasswordEncoder passwordEncoder;

    @Resource
    public TokenStore tokenStore;

    @Resource(name = "userDetailsServiceBean")
    public UserDetailsService userDetailsService;

    @Value("${security.oauth2.client.access-token-uri}")
    private String accessTokenUri;

    @Value("${oauth2.grant_type}")
    public String oauth2GrantType;

    @Value("${oauth2.client_id}")
    public String oauth2ClientId;

    @Value("${oauth2.client_secret}")
    public String oauth2ClientSecret;

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    AuthUserMapper userMapper;

    @Override
    public ResponseResult login(LoginPojo loginPojo) {
    
    
        Map<String, Object> result = new HashMap<>();
        AuthUser user = userMapper.getInfoByName(loginPojo.getUsername());
        if (user == null) {
    
    
            return new ResponseResult(ResponseCode.FAILURE, "账号不存在!");
        }
        if (user.getIsDeleted()) {
    
    
            return new ResponseResult(ResponseCode.FAILURE, "账号已被删除,请联系管理员!");
        }
        UserDetails userDetails = userDetailsService.loadUserByUsername(loginPojo.getUsername());
        if (userDetails == null || !passwordEncoder.matches(loginPojo.getPassword(), userDetails.getPassword())) {
    
    
            return new ResponseResult(ResponseCode.FAILURE, "账号或密码错误!");
        }
        if (!user.getIsEnable()) {
    
    
            return new ResponseResult(ResponseCode.FAILURE, "账号未激活,请联系管理员!");
        }
        String accessToken = getToken(loginPojo.getUsername(), loginPojo.getPassword());
        result.put("access_token", accessToken);
        result.put("userId", user.getUuid());
        return new ResponseResult(ResponseCode.SUCCESS, result);
    }


    @Override
    public ResponseResult logout(String accessToken) {
    
    
        try {
    
    
            OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);
            tokenStore.removeAccessToken(oAuth2AccessToken);
            return new ResponseResult(ResponseCode.SUCCESS);
        } catch (Exception e) {
    
    
            return new ResponseResult(ResponseCode.FAILURE);
        }
    }

    @Override
    public Map<String, String> refresh(String accessToken) {
    
    
        Map<String, String> result = new HashMap<>();
        //Access Token 不存在在直接返回null
        String refreshToken = (String) redisTemplate.opsForValue().get(accessToken);
        if (StrUtil.isBlank(refreshToken)) {
    
    
            throw new BizException(ResponseCode.USER_NOT_LOGGED_IN);
        }
        //通过HTTP客户端请求登录接口
        Map<String, Object> authParam = getAuthParam();
        authParam.put("grant_type", "refresh_token");
        authParam.put("refresh_token", refreshToken);

        //获取access_token
        String strJson = HttpUtil.post(accessTokenUri, authParam);
        JSONObject jsonObject = JSONUtil.parseObj(strJson);
        //新的access_token和refresh_token
        String access_token = String.valueOf(jsonObject.get("access_token"));
        String refresh_token = String.valueOf(jsonObject.get("refresh_token"));
        if (StrUtil.isNotBlank(access_token) && StrUtil.isNotBlank(refreshToken)) {
    
    
            //删除旧Token
            redisTemplate.delete(accessToken);
            //将refresh_token 保存到redis
            redisTemplate.opsForValue().set(access_token, refresh_token);
            result.put("access_token", access_token);
            return result;
        }
        return null;
    }

    /**
     * 获取用户token
     *
     * @param username 用户名
     * @param password 密码
     * @return String
     */
    private String getToken(String username, String password) {
    
    
        Map<String, String> result = new HashMap<>();
        //通过HTTP客户端请求登录接口
        Map<String, Object> authParam = getAuthParam();
        authParam.put("username", username);
        authParam.put("password", password);
        authParam.put("grant_type", oauth2GrantType);
        //获取access_token
        String strJson = HttpUtil.post(accessTokenUri, authParam);
        JSONObject jsonObject = JSONUtil.parseObj(strJson);
        String accessToken = String.valueOf(jsonObject.get("access_token"));
        String refreshToken = String.valueOf(jsonObject.get("refresh_token"));
        if (StrUtil.isNotBlank(accessToken) && StrUtil.isNotBlank(refreshToken)) {
    
    
            //将refresh_token保存在redis,设置超时时间为24小时
            redisTemplate.opsForValue().set(accessToken, refreshToken, 24, TimeUnit.HOURS);
            //将access_token返回
            return accessToken;
        }
        return null;
    }

    private Map<String, Object> getAuthParam() {
    
    
        Map<String, Object> param = new HashMap<>();
        param.put("client_id", oauth2ClientId);
        param.put("client_secret", oauth2ClientSecret);
        return param;
    }
}

部分类没有列出来,请根据自己实际情况调整代码,核心代码都有注释

测试用户登录

image-20210527225855655

注意

我的Demo项目中的接口权限地址配置在数据库中了,资源服务器可以动态获取可访问的资源url

数据库截图如下:

image-20210527231119052

  • ** 表示/test/路由下的所有接口都需要test权限才能访问

猜你喜欢

转载自blog.csdn.net/qq_36213352/article/details/117340214