这里写自定义目录标题
- 设计理念
- 技术栈
- 后端
- 前端
- 项目结构
- 数据库说明
- 前端页面及操作
- 最终效果
- 项目地址
设计理念
- 权限框架选择:一般springboot类型项目的权限系统shiro或者spring-security都能满足。现在越来越多的系统都是前后端分离,且后端很多是springcloud框架,无疑oauth2是个更好的选择。
- 前端预期效果:所有前端请求都经过网关再调取后台服务,如果用户未登录或者登陆过期返回401,跳转至登陆页面,登陆成功拿到后端返回的access_token存在localStorage,未授权的请求返回403提示未授权,其他不需要授权的页面或者请求可以直接访问,前端axios请求加拦截器,请求时从localStorage获取token,将token添加到请求头中。
- 后端逻辑处理:每个前端请求当成是一个资源,一个资源是否可以访问由当前用户是否拥有可以访问该资源的权限决定。一个用户对应多个角色,一个角色对应多个权限,一个资源对应多个权限。
- 数据库设计:t_user(用户表)、t_role(角色表)、t_authority(权限表)、t_resource(资源表)、t_user_role(用户角色表)、t_role_auhority(角色权限表)、t_resource_authority(资源权限表)
技术栈
后端
- springcloud:后端框架
- nacos:注册跟配置
- oauth2:权限(密码模式)
- spring-security:权限配合使用
- jwt:access_token转化格式
- gateway:网关springcloud-gateway
- mybatis-plus:持久层框架,mybatis加强版
- mysql:数据库
- redisson:token存储,redis的加强版
前端
- vue
项目结构
后端
说明
- cloud-parent:父工程
pom.xml
<?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>
<packaging>pom</packaging>
<modules>
<module>cloud-auth</module>
<module>cloud-gateway</module>
<module>cloud-api</module>
<module>common-web</module>
<module>cloud-system</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.spring.cloud</groupId>
<artifactId>cloud-parent</artifactId>
<version>0.0.1</version>
<name>cloud-parent</name>
<description>Demo project for Spring Cloud</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- cloud-api:简单web工程(为了测试权限)
- cloud-auth:认证服务,用户认证,token存储等
pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-parent</artifactId>
<groupId>com.spring.cloud</groupId>
<version>0.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-auth</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.16</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.4</version>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 88
spring:
application:
name: cloud-auth
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud-auth?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
cloud:
nacos:
discovery:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: "*"
logging:
level:
com.spring.cloud.auth.mapper: debug
KeyPairController
@RestController
public class KeyPairController {
private final KeyPair keyPair;
public KeyPairController(KeyPair keyPair) {
this.keyPair = keyPair;
}
@GetMapping("/rsa/publicKey")
public Map<String, Object> getKey() {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}
}
Oauth2AuthorizationAdapter :权限适配器
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthorizationAdapter extends AuthorizationServerConfigurerAdapter {
@Autowired
private UserServiceImpl userDetailsService;
@Autowired
private TokenStore tokenStore;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Autowired
private JdbcClientDetailsService clientDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//客户端详情:数据库方式
clients.withClientDetails(clientDetailsService);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
//密码模式要带authenticationManager参数
.authenticationManager(authenticationManager)
//自定义userDetailsService
.userDetailsService(userDetailsService)
//配置令牌存储策略
.tokenStore(tokenStore)
//token转换器
.accessTokenConverter(accessTokenConverter);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
//允许表单认证
security.allowFormAuthenticationForClients();
}
}
WebSecurityConfig :WebSecurityConfig 配置类
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.antMatchers("/rsa/publicKey").permitAll()
.anyRequest()
.authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public TokenStore tokenStore(RedisConnectionFactory factory) {
return new RedisTokenStore(factory);
}
@Bean
public JdbcClientDetailsService jdbcClientDetailsService(DataSource dataSource) {
return new JdbcClientDetailsService(dataSource);
}
@Bean
public JwtAccessTokenConverter accessTokenConverter(KeyPair keyPair) {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair);
return jwtAccessTokenConverter;
}
@Bean
public KeyPair keyPair() {
//从classpath下的证书中获取秘钥对
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetails superAdmin(PasswordEncoder passwordEncoder, UserMapper userMapper) {
List<GrantedAuthority> authorities = new ArrayList<>();
List<String> userAuthorities = userMapper.getUserAuthorities(null);
userAuthorities.forEach(authority -> authorities.add(new SimpleGrantedAuthority(authority)));
return new User("spring_cloud_oauth2", passwordEncoder.encode("123456"), authorities);
}
}
UserServiceImpl :UserDetailsService 数据库实现方式
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetails superAdmin;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//默认设置了一个拥有所有权限的用户
if(superAdmin.getUsername().equals(username)){
return superAdmin;
}
LambdaQueryWrapper<UserEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserEntity::getUsername, username);
UserEntity userEntity = userMapper.selectOne(wrapper);
if (userEntity == null) {
throw new InvalidGrantException("用户名或密码错误");
}
List<String> userAuthorities = userMapper.getUserAuthorities(userEntity.getId());
List<GrantedAuthority> authorities = new ArrayList<>();
userAuthorities.forEach(authority -> authorities.add(new SimpleGrantedAuthority(authority)));
return new User(username, passwordEncoder.encode(userEntity.getPassword()), authorities);
}
}
UserMapper
public interface UserMapper extends BaseMapper<UserEntity> {
List<String> getUserAuthorities(Integer userId);
}
UserMapper.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.spring.cloud.auth.mapper.UserMapper">
<select id="getUserAuthorities" resultType="java.lang.String">
SELECT DISTINCT a.authority_name
FROM t_user u
LEFT JOIN t_user_role ur ON ur.user_id = u.id
LEFT JOIN t_role r ON r.id = ur.role_id
LEFT JOIN t_role_authority ra ON ra.role_id = r.id
LEFT JOIN t_authority a ON a.id = ra.authority_id
WHERE a.authority_name IS NOT NULL
<if test="userId != null">
AND u.id = #{userId}
</if>
</select>
</mapper>
- cloud-gateway:资源服务,权限校验
pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-parent</artifactId>
<groupId>com.spring.cloud</groupId>
<version>0.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-gateway</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 90
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud-auth?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
application:
name: cloud-gateway
cloud:
nacos:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: api #唯一标识
uri: lb://cloud-api #转发的地址,写服务名称
predicates:
- Path=/api/** #判断匹配条件,即地址带有/api/**的请求,会转发至lb:cloud-api
filters:
- StripPrefix=1
- id: auth
uri: lb://cloud-auth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
- id: system
uri: lb://cloud-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: 'http://localhost:88/rsa/publicKey' #配置RSA的公钥访问地址
logging:
level:
com.spring.cloud.gateway.mapper: debug
ResourceServerConfig:资源服务配置文件
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter,
GatewayReactiveAuthorizationManager manager) {
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter);
http.authorizeExchange()
//.pathMatchers().permitAll()的方式不能动态授权白名单,改为manager手动授权
.anyExchange()
.access(manager)
.and().exceptionHandling()
.and().csrf().disable();
return http.build();
}
}
GatewayReactiveAuthorizationManager :权限管理器
@Configuration
public class GatewayReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private final static String AUTHORITY_PREFIX = "ROLE_";
@Autowired
private ResourceMapper resourceMapper;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
String url = authorizationContext.getExchange().getRequest().getURI().getPath();
String method = authorizationContext.getExchange().getRequest().getMethodValue();
//根据请求方式和url获取当前资源
ResourceEntity resourceEntity = resourceMapper.getResourceEntity(method, url);
if (resourceEntity != null) {
//如果该资源permitAll,直接返回true
if (resourceEntity.getPermitAll() != null && resourceEntity.getPermitAll() == 1) {
return Mono.just(new AuthorizationDecision(true));
}
List<String> authorities = new ArrayList<>();
//获取该资源能够访问的权限
List<String> accessAuthorities = resourceMapper.getAccessAuthorities(resourceEntity.getId());
if (!CollectionUtils.isEmpty(accessAuthorities)) {
for (String authority : accessAuthorities) {
authorities.add(AUTHORITY_PREFIX + authority);
}
}
return mono
//用户是否已经认证,没有返回401(token没有或者失效)
.filter(Authentication::isAuthenticated)
//获取用户的权限
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
//资源权限跟用户拥有的权限匹配,有一个满足即可
.any(authorities::contains)
.map(AuthorizationDecision::new)
//为空返回false
.defaultIfEmpty(new AuthorizationDecision(false));
}
//为空返回false,即403(forbidden)
return Mono.just(new AuthorizationDecision(false));
}
@Bean
public Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AUTHORITY_PREFIX);
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
}
ResourceMapper
public interface ResourceMapper extends BaseMapper<ResourceEntity> {
/**
* Description 如果一个url跟method匹配到多个会报错
*
* @param method 请求方式
* @param url url
* @author qinchao
* @date 2021/4/28 14:30
* @return com.spring.cloud.gateway.entity.ResourceEntity
*/
ResourceEntity getResourceEntity(String method, String url);
/**
* Description 获取资源所能访问的权限
*
* @param resourceId
* @author qinchao
* @date 2021/4/28 14:31
* @return java.util.List<java.lang.String>
*/
List<String> getAccessAuthorities(Integer resourceId);
}
ResourceMapper.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.spring.cloud.gateway.mapper.ResourceMapper">
<select id="getResourceEntity" resultType="com.spring.cloud.gateway.entity.ResourceEntity">
SELECT r.*
FROM t_resource r
WHERE r.method = #{method}
AND #{url} regexp concat(r.module,r.url)
</select>
<select id="getAccessAuthorities" resultType="string">
SELECT a.`authority_name`
FROM t_resource_authority ra
LEFT JOIN t_authority a ON a.id = ra.authority_id
WHERE ra.resource_id = #{resourceId}
</select>
</mapper>
- cloud-system:权限管理服务,用户、角色、权限、资源的增删改查
- common:web通用工程,常用工具配置
前端
数据库说明
- oauth_client_details:oauth2的数据库实现方式
- t_user、t_role、t_authority:用户角色权限及其关联关系表比较简单就不赘述了(去掉一层role,即直接用户权限关系也是可以的)
- t_resource:资源表(重点)
resource_type:该资源是页面还是ajax,module:请求所属模块,url:请求地址,method:请求方式,permit_all:该资源是否直接开放(比如登陆页和登陆接口为1,直接放行)
前端页面及操作
- 登陆页面:直接放行;登陆接口直接放行
- Hello页面:直接放行
- api页面:测试、超管、系统管理员可访问;里面3个按钮,hello直接放行,test测试能访问,admin超管能访问(以上3个页面及其按钮操作已经可以测试权限效果了)
- 系统管理:对用户、角色、权限、资源的增删改查,系统管理员跟超管能访问(对应的后台服务cloud-system)
- 退出:localStorage删除token,跳转登陆页即可,后端不用任何处理
最终效果
当访问未授权的页面获取请求时
当未登陆或者登陆过期时
后端:cloud-oauth2前端