文章目录
SpringBoot单体服务版配置 Oauth2.0
- 数据库:PostgresSQL
- 开发工具:idea
- 开发框架:SpringBoot
- 数据库操作:MyBatisPlus
- 实体类工具:lombok
数据库配置
RBAC 是基于角色的访问控制(Role-Based Access Control
)在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
用户表
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;
代码
项目结构
- 启用密码模式
- 使用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
接口,代码如下:
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;
}
}
部分类没有列出来,请根据自己实际情况调整代码,核心代码都有注释
测试用户登录
注意
我的Demo项目中的接口权限地址配置在数据库中了,资源服务器可以动态获取可访问的资源url
数据库截图如下:
- ** 表示/test/路由下的所有接口都需要test权限才能访问