1  项目介绍

最开始是一个单体应用,所有功能模块都写在一个项目里,后来觉得项目越来越大,于是决定把一些功能拆分出去,形成一个一个独立的微服务,于是就有个问题了,登录、退出、权限控制这些东西怎么办呢?总不能每个服务都复制一套吧,最好的方式是将认证与鉴权也单独抽离出来作为公共的服务,业务系统只专心做业务接口开发即可,完全不用理会权限这些与之不相关的东西了。于是,便有了下面的架构图:

下面重点看一下统一认证中心和业务网关的建设

2  统一认证中心

这里采用 Spring Security + Spring Security OAuth2
OAuth2是一种认证授权的协议,是一种开放的标准。最长用到的是授权码模式和密码模式,在本例中,用这两种模式都可以。
首先,引入相关依赖
最主要的依赖是 spring-cloud-starter-oauth2 ,引入它就够了

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>

这里Spring Boot的版本是2.6.3

完整的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>com.tgf</groupId>
<artifactId>tgf-service-parent</artifactId>
<version>1.3.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.soa.supervision.uaa</groupId>
<artifactId>soas-uaa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>soas-uaa</name>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.0</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.19</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.mybatis.scripting</groupId>
<artifactId>mybatis-freemarker</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

配置授权服务器

在授权服务器中,主要是配置如何生成Token,以及注册的客户端有哪些

package com.soa.supervision.uaa.config;

import com.soa.supervision.uaa.constant.AuthConstants;
import com.soa.supervision.uaa.domain.SecurityUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
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.OAuth2Authentication;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.endpoint.TokenKeyEndpoint;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; import javax.annotation.Resource;
import javax.sql.DataSource;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map; /**
* 授权服务器配置
* 1、配置客户端
* 2、配置Access_Token生成
*
* @Author ChengJianSheng
* @Date 2022/2/14
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Resource
private DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager; @Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(new JdbcClientDetailsService(dataSource));
} @Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
// security.tokenKeyAccess("permitAll()");
} @Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
List<TokenEnhancer> tokenEnhancerList = new ArrayList<>();
tokenEnhancerList.add(jwtTokenEnhancer());
tokenEnhancerList.add(jwtAccessTokenConverter());
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(tokenEnhancerList); endpoints.accessTokenConverter(jwtAccessTokenConverter())
.tokenEnhancer(tokenEnhancerChain).authenticationManager(authenticationManager);
} /**
* Token增强
*/
public TokenEnhancer jwtTokenEnhancer() {
return new TokenEnhancer() {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
Map<String, Object> additionalInformation = new HashMap<>();
additionalInformation.put(AuthConstants.JWT_USER_ID_KEY, securityUser.getUserId());
additionalInformation.put(AuthConstants.JWT_USER_NAME_KEY, securityUser.getUsername());
additionalInformation.put(AuthConstants.JWT_DEPT_ID_KEY, securityUser.getDeptId());
((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(additionalInformation);
return accessToken;
}
};
} /**
* 采用RSA加密算法对JWT进行签名
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair());
return jwtAccessTokenConverter;
} /**
* 密钥对
*/
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
} @Bean
public TokenKeyEndpoint tokenKeyEndpoint() {
return new TokenKeyEndpoint(jwtAccessTokenConverter());
}
}

说明:

  • 客户端是从数据库加载的
  • 密码模式下必须设置一个AuthenticationManager
  • 采用JWT生成token是因为它轻量级,无需存储可以减小服务端的存储压力。但是,为了实现退出功能,不得不将它存储到Redis中
  • 必须要对JWT进行加密,资源服务器在拿到客户端传的token时会去校验该token是否合法,否则客户端可能伪造token
  • 此处对token进行了增强,在token中加了几个字段分别表示用户ID和部门ID



    客户端表结构如下:
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端ID',
`resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户端密钥',
`scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '授权类型',
`web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`access_token_validity` int(11) NULL DEFAULT NULL COMMENT 'access_token的有效时间',
`refresh_token_validity` int(11) NULL DEFAULT NULL COMMENT 'refresh_token的有效时间',
`additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否允许自动授权',
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; INSERT INTO `oauth_client_details` VALUES ('hello', 'order-resource', '$2a$10$1Vun/h63tI4C48BqLsy2Zel5q5M2VW6w8KThoMfxww49wf9uv/dKy', 'all', 'authorization_code,password,refresh_token', 'http://www.baidu.com', NULL, 7200, 7260, NULL, 'true');
INSERT INTO `oauth_client_details` VALUES ('sso-client-1', NULL, '$2a$10$CxEwmODmsp/HOB7LloeBJeqUjotmNzjpk2WmjxtPxAeOYifQWLfhW', 'all', 'authorization_code', 'http://localhost:9001/sso-client-1/login/oauth2/code/custom', NULL, 180, 240, NULL, 'true');

本例中采用RSA非对称加密,密钥文件用的是java自带的keytools生成的



将来,认证服务器用私钥对token加密,然后将公钥公开

package com.soa.supervision.uaa.controller;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.util.Map; /**
* @Author ChengJianSheng
* @Date 2022/2/15
*/
@RestController
public class KeyPairController { @Autowired
private KeyPair keyPair; @GetMapping("/rsa/publicKey")
public Map<String, Object> getKey() {
RSAPublicKey publicKey = (RSAPublicKey) this.keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}
}

配置WebSecurity

在WebSecurity中主要是配置用户,以及哪些请求需要认证以后才能访问

package com.soa.supervision.uaa.config;

import com.soa.supervision.uaa.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; /**
* @Author ChengJianSheng
* @Date 2022/2/14
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired
private UserDetailsServiceImpl userDetailsService; @Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.antMatchers("/rsa/publicKey", "/menu/tree").permitAll()
.anyRequest().authenticated()
.and().formLogin().permitAll()
.and()
.csrf().disable();
} @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
} @Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
} @Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

UserDetailsService实现类

package com.soa.supervision.uaa.service.impl;

import com.soa.supervision.uaa.domain.AuthUserDTO;
import com.soa.supervision.uaa.domain.SecurityUser;
import com.soa.supervision.uaa.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException;
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 java.util.Set;
import java.util.stream.Collectors; /**
* @Author ChengJianSheng
* @Date 2022/2/14
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService { @Autowired
private SysUserService sysUserService; @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AuthUserDTO authUserDTO = sysUserService.getAuthUserByUsername(username);
if (null == authUserDTO) {
throw new UsernameNotFoundException("用户不存在");
}
if (!authUserDTO.isEnabled()) {
throw new LockedException("账号被禁用");
}
Set<SimpleGrantedAuthority> authorities = authUserDTO.getRoles().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
return new SecurityUser(authUserDTO.getUserId(), authUserDTO.getDeptId(), authUserDTO.getUsername(), authUserDTO.getPassword(), authUserDTO.isEnabled(), authorities);
}
}

SysUserService

package com.soa.supervision.uaa.service;

import com.soa.supervision.uaa.domain.AuthUserDTO;
import com.soa.supervision.uaa.entity.SysUser;
import com.baomidou.mybatisplus.extension.service.IService; /**
* <p>
* 用户表 服务类
* </p>
*
* @author ChengJianSheng
* @since 2022-02-14
*/
public interface SysUserService extends IService<SysUser> {
AuthUserDTO getAuthUserByUsername(String username);
}

AuthUserDTO

package com.soa.supervision.uaa.domain;

import lombok.Data;

import java.io.Serializable;
import java.util.List; /**
* @Author ChengJianSheng
* @Date 2022/2/15
*/
@Data
public class AuthUserDTO implements Serializable {
private Integer userId;
private String username;
private String password;
private Integer deptId;
private boolean enabled;
private List<String> roles;
}

SysUserServiceImpl

package com.soa.supervision.uaa.service.impl;

import com.soa.supervision.uaa.domain.AuthUserDTO;
import com.soa.supervision.uaa.entity.SysUser;
import com.soa.supervision.uaa.mapper.SysUserMapper;
import com.soa.supervision.uaa.service.SysUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; /**
* <p>
* 用户表 服务实现类
* </p>
*
* @author ChengJianSheng
* @since 2022-02-14
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService { @Autowired
private SysUserMapper sysUserMapper; @Override
public AuthUserDTO getAuthUserByUsername(String username) {
return sysUserMapper.selectAuthUserByUsername(username);
}
}

SysUserMapper

package com.soa.supervision.uaa.mapper;

import com.soa.supervision.uaa.domain.AuthUserDTO;
import com.soa.supervision.uaa.entity.SysUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; /**
* 用户表 Mapper 接口
*
* @author ChengJianSheng
* @since 2022-02-14
*/
public interface SysUserMapper extends BaseMapper<SysUser> {
AuthUserDTO selectAuthUserByUsername(String username);
}

SysUserMapper.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.soa.supervision.uaa.mapper.SysUserMapper"> <resultMap id="authUserResultMap" type="com.soa.supervision.uaa.domain.AuthUserDTO">
<id property="userId" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="deptId" column="dept_id"/>
<result property="enabled" column="enabled"/>
<collection property="roles" ofType="string" javaType="list">
<result column="role_code"/>
</collection>
</resultMap> <!-- 根据用户名查用户 -->
<select id="selectAuthUserByUsername" resultMap="authUserResultMap">
SELECT
t1.id,
t1.username,
t1.`password`,
t1.dept_id,
t1.enabled,
t3.`code` AS role_code
FROM
sys_user t1
LEFT JOIN sys_user_role t2 ON t1.id = t2.user_id
LEFT JOIN sys_role t3 ON t2.role_id = t3.id
WHERE
t1.username = #{username}
</select> </mapper>

UserDetails

package com.soa.supervision.uaa.domain;

import lombok.AllArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection;
import java.util.Set; /**
* @Author ChengJianSheng
* @Date 2022/2/14
*/
@AllArgsConstructor
public class SecurityUser implements UserDetails {
/**
* 扩展字段
*/
private Integer userId;
private Integer deptId; private String username;
private String password;
private boolean enabled;
private Set<SimpleGrantedAuthority> authorities; @Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
} @Override
public String getPassword() {
return password;
} @Override
public String getUsername() {
return username;
} @Override
public boolean isAccountNonExpired() {
return true;
} @Override
public boolean isAccountNonLocked() {
return true;
} @Override
public boolean isCredentialsNonExpired() {
return true;
} @Override
public boolean isEnabled() {
return enabled;
} public Integer getUserId() {
return userId;
} public Integer getDeptId() {
return deptId;
}
}

登录

默认的登录url是/login,本例中没有自定义登录页面,而是使用默认的登录页面

正常的密码模式下,输入用户名和密码,登录成功以后返回token。本例中使用密码模式,所以写了个登录接口,而且也是取巧,覆盖了默认的/oauth/token端点

package com.soa.supervision.uaa.controller;

import com.tgf.common.domain.RespResult;
import com.tgf.common.util.RespUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.*; import java.security.Principal;
import java.util.HashMap;
import java.util.Map; /**
* @Author ChengJianSheng
* @Date 2022/2/18
*/
@RestController
@RequestMapping("/oauth")
public class AuthorizationController { @Autowired
private TokenEndpoint tokenEndpoint; /**
* 密码模式 登录
* @param principal
* @param parameters
* @return
* @throws HttpRequestMethodNotSupportedException
*/
@PostMapping("/token")
public RespResult postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
Map<String, Object> map = new HashMap<>();
// 缓存
return RespUtils.success();
} /**
* 退出
* @return
*/
@PostMapping("/logout")
public RespResult logout() { // JSONObject payload = JwtUtils.getJwtPayload();
// String jti = payload.getStr(SecurityConstants.JWT_JTI); // JWT唯一标识
// Long expireTime = payload.getLong(SecurityConstants.JWT_EXP); // JWT过期时间戳(单位:秒)
// if (expireTime != null) {
// long currentTime = System.currentTimeMillis() / 1000;// 当前时间(单位:秒)
// if (expireTime > currentTime) { // token未过期,添加至缓存作为黑名单限制访问,缓存时间为token过期剩余时间
// redisTemplate.opsForValue().set(SecurityConstants.TOKEN_BLACKLIST_PREFIX + jti, null, (expireTime - currentTime), TimeUnit.SECONDS);
// }
// } else { // token 永不过期则永久加入黑名单
// redisTemplate.opsForValue().set(SecurityConstants.TOKEN_BLACKLIST_PREFIX + jti, null);
// }
// return Result.success("注销成功"); return RespUtils.success();
}
}

补充:授权码模式获取access_token

菜单

登录以后,前端会查询菜单并展示,下面是菜单相关接口

SysMenuController

package com.soa.supervision.uaa.controller;

import com.soa.supervision.uaa.domain.MenuVO;
import com.soa.supervision.uaa.service.SysMenuService;
import com.tgf.common.domain.RespResult;
import com.tgf.common.util.RespUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Arrays;
import java.util.List; /**
* <p>
* 菜单表 前端控制器
* </p>
*
* @author ChengJianSheng
* @since 2022-02-21
*/
@RestController
@RequestMapping("/menu")
public class SysMenuController { @Autowired
private SysMenuService sysMenuService; @GetMapping("/tree")
public RespResult tree(String systemCode) {
List<Integer> roleIds = Arrays.asList(1,2);
List<MenuVO> voList = sysMenuService.getMenuByUserRoles(systemCode, roleIds);
return RespUtils.success(voList);
}
}

SysMenuService

package com.soa.supervision.uaa.service;

import com.soa.supervision.uaa.domain.MenuVO;
import com.soa.supervision.uaa.entity.SysMenu;
import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /**
* <p>
* 菜单表 服务类
* </p>
*
* @author ChengJianSheng
* @since 2022-02-21
*/
public interface SysMenuService extends IService<SysMenu> {
List<MenuVO> getMenuByUserRoles(String systemCode, List<Integer> roleIds);
}

SysMenuServiceImpl

package com.soa.supervision.uaa.service.impl;

import com.soa.supervision.uaa.domain.MenuVO;
import com.soa.supervision.uaa.entity.SysMenu;
import com.soa.supervision.uaa.mapper.SysMenuMapper;
import com.soa.supervision.uaa.service.SysMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors; /**
* <p>
* 菜单表 服务实现类
* </p>
*
* @author ChengJianSheng
* @since 2022-02-21
*/
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService { @Autowired
private SysMenuMapper sysMenuMapper; /**
* 构造菜单树
* @param systemCode
* @param roleIds
* @return
*/
@Override
public List<MenuVO> getMenuByUserRoles(String systemCode, List<Integer> roleIds) {
List<MenuVO> voList = new ArrayList<>(); List<SysMenu> sysMenuList = sysMenuMapper.selectMenuByRole(systemCode, roleIds);
if (null == sysMenuList || sysMenuList.size() == 0) {
return voList;
}
List<MenuVO> menuVOList = sysMenuList.stream().map(e->{
MenuVO vo = new MenuVO();
BeanUtils.copyProperties(e, vo);
vo.setChildren(new ArrayList<>());
return vo;
}).distinct().collect(Collectors.toList()); for (int i = 0; i < menuVOList.size(); i++) {
for (int j = 0; j < menuVOList.size(); j++) {
if (menuVOList.get(i).getId().equals(menuVOList.get(j).getId())) {
continue;
}
if (menuVOList.get(i).getId().equals(menuVOList.get(j).getParentId())) {
menuVOList.get(i).getChildren().add(menuVOList.get(j));
}
}
} return menuVOList.stream().filter(e->0==e.getParentId()).collect(Collectors.toList());
}
}

MenuVO

package com.soa.supervision.uaa.domain;

import lombok.Data;

import java.io.Serializable;
import java.util.List; /**
* @Author ChengJianSheng
* @Date 2022/2/21
*/
@Data
public class MenuVO implements Serializable { private Integer id; /**
* 菜单名称
*/
private String name; /**
* 父级菜单ID
*/
private Integer parentId; /**
* 路由地址
*/
private String routePath; /**
* 组件
*/
private String component; /**
* 图标
*/
private String icon; /**
* 排序号
*/
private Integer sort; /**
* 子菜单
*/
private List<MenuVO> children;
}

SysMenuMapper

package com.soa.supervision.uaa.mapper;

import com.soa.supervision.uaa.entity.SysMenu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param; import java.util.List; /**
* <p>
* 菜单表 Mapper 接口
* </p>
*
* @author ChengJianSheng
* @since 2022-02-21
*/
public interface SysMenuMapper extends BaseMapper<SysMenu> {
List<SysMenu> selectMenuByRole(@Param("systemCode") String systemCode, @Param("roleIds") List<Integer> roleIds);
}

SysMenuMapper.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.soa.supervision.uaa.mapper.SysMenuMapper"> <!-- 根据角色查菜单 -->
<select id="selectMenuByRole" resultType="com.soa.supervision.uaa.entity.SysMenu">
SELECT
t1.*
FROM
sys_menu t1
LEFT JOIN sys_role_menu t2 ON t1.id = t2.menu_id
WHERE
t1.system_code = #{systemCode}
AND t1.hidden = 0
AND t2.role_id IN <foreach collection="roleIds" item="roleId" open="(" close=")" separator=",">#{roleId}</foreach>
ORDER BY
t1.sort ASC
</select> </mapper>

application.yml

server:
port: 8094
servlet:
context-path: /soas-uaa
spring:
application:
name: soas-uaa
datasource:
url: jdbc:mysql://192.168.28.22:3306/demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 1234567
redis:
host: 192.168.28.01
port: 6379
password: 123456
logging:
level:
org:
springframework:
security: debug
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3  网关

在这里,网关相当于OAuth2中的资源服务器这么个角色。网关代理了所有的业务微服务,如果说那些业务服务是资源的,那么网关就是资源的集合,访问网关就是访问资源,访问资源就要先认证再授权才能访问。同时,网关又相当于一个公共方法,因此在这里做鉴权是比较合适的。

首先是依赖

<?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>com.tgf</groupId>
<artifactId>tgf-service-parent</artifactId>
<version>1.3.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.soa.supervision.gateway</groupId>
<artifactId>soas-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>soas-gateway</name>
<properties>
<java.version>1.8</java.version>
<spring-security.version>5.6.1</spring-security.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>${spring-security.version}</version>
</dependency>
<!-- spring-security-oauth2-jose的依赖中包含了nimbus-jose-jwt,只是版本不是最新的而已,这里如果想使用更高版本的nimbus-jose-jwt的话可以重新声明一下 -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.15.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.21</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

application.yml

server:
port: 8090
spring:
cloud:
gateway:
routes:
- id: soas-enterprise
uri: http://127.0.0.1:8093
predicates:
- Path=/soas-enterprise/**
- id: soas-portal
uri: http://127.0.0.1:8092
predicates:
- Path=/soas-portal/**
- id: soas-finance
uri: http://127.0.0.1:8095
predicates:
- Path=/soas-finance/**
discovery:
locator:
enabled: false
redis:
host: 192.168.28.01
port: 6379
password: 123456
database: 9
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:8094/soas-uaa/rsa/publicKey
secure:
ignore:
urls:
- /soas-portal/auth/**

直接放行的url

package com.soa.supervision.gateway.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; /**
* @Author ChengJianSheng
* @Date 2021/12/15
*/
@Data
@Component
@ConfigurationProperties(prefix = "secure.ignore")
public class IgnoreUrlProperties {
private String[] urls;
}

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds" debug="false">
<property name="log.charset" value="utf-8" />
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" />
<property name="log.dir" value="./logs" /> <!--输出到控制台-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
<charset>${log.charset}</charset>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.dir}/soas-gateway.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.dir}/soas-gateway.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender> <root level="info">
<appender-ref ref="console" />
<appender-ref ref="file" />
</root>
</configuration>

鉴权

真正的权限判断或者说权限控制是在这里,下面这段代码尤为重要,而且它在整个网关过滤器之前调用

package com.soa.supervision.gateway.config;

import com.alibaba.fastjson.JSON;
import com.soa.supervision.gateway.constant.AuthConstants;
import com.soa.supervision.gateway.constant.RedisConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono; import java.util.ArrayList;
import java.util.List;
import java.util.Map; /**
* @Author ChengJianSheng
* @Date 2022/2/16
*/
@Slf4j
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> { private final PathMatcher pathMatcher = new AntPathMatcher(); @Autowired
private StringRedisTemplate stringRedisTemplate; @Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext context) {
ServerHttpRequest request = context.getExchange().getRequest();
String path = request.getURI().getPath(); // token不能为空且有效
String token = request.getHeaders().getFirst(AuthConstants.JWT_TOKEN_HEADER);
if (StringUtils.isBlank(token) || !token.startsWith(AuthConstants.JWT_TOKEN_PREFIX)) {
return Mono.just(new AuthorizationDecision(false));
} String realToken = token.trim().substring(7);
Long ttl = stringRedisTemplate.getExpire(RedisConstants.ONLINE_TOKEN_PREFIX_KV + realToken);
if (ttl <= 0) {
return Mono.just(new AuthorizationDecision(false));
} // 获取访问资源所需的角色
List<String> authorizedRoles = new ArrayList<>(); // 拥有访问权限的角色
Map<Object, Object> urlRoleMap = stringRedisTemplate.opsForHash().entries(RedisConstants.URL_ROLE_MAP_HK);
for (Map.Entry<Object, Object> entry : urlRoleMap.entrySet()) {
String permissionUrl = (String) entry.getKey();
List<String> roles = JSON.parseArray((String) entry.getValue(), String.class);
if (pathMatcher.match(permissionUrl, path)) {
authorizedRoles.addAll(roles);
}
}
// 没有配置权限规则表示无需授权,直接放行
if (CollectionUtils.isEmpty(authorizedRoles)) {
return Mono.just(new AuthorizationDecision(true));
} // 判断用户拥有的角色是否可以访问资源
return authentication.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority).any(authorizedRoles::contains)
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
} }

菜单权限在Redis中是这样存储的

url -> [角色编码, 角色编码, 角色编码]

查询SQL

SELECT
t1.url,
t3.`code` AS role_code
FROM
sys_menu t1
LEFT JOIN sys_role_menu t2 ON t1.id = t2.menu_id
LEFT JOIN sys_role t3 ON t2.role_id = t3.id
WHERE t1.url is NOT NULL;

存储到Redis

HSET "/soas-order/order/pageList" "[\"admin\",\"org\"]"
HSET "/soas-order/order/save" "[\"admin\",\"enterprise\"]"

资源访问的一些配置

ResourceServerConfig

package com.soa.supervision.gateway.config;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.IoUtil;
import com.soa.supervision.gateway.util.ResponseUtils;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import java.io.InputStream;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec; /**
* @Author ChengJianSheng
* @Date 2022/02/15
*/
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig { @Autowired
private IgnoreUrlProperties ignoreUrlProperties;
@Autowired
private AuthorizationManager authorizationManager; @Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// 配置JWT解码相关
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());//.publicKey(rsaPublicKey()); http.authorizeExchange()
.pathMatchers(ignoreUrlProperties.getUrls()).permitAll()
.anyExchange().access(authorizationManager)
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.authenticationEntryPoint(authenticationEntryPoint())
.and()
.csrf().disable(); return http.build();
} public Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
// jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
} /**
* 未授权(没有访问权限)
*/
public ServerAccessDeniedHandler accessDeniedHandler() {
return (ServerWebExchange exchange, AccessDeniedException denied) -> {
Mono<Void> mono = Mono.defer(() -> Mono.just(exchange.getResponse())).flatMap(resp -> ResponseUtils.writeErrorInfo(resp, HttpStatus.UNAUTHORIZED));
return mono;
};
} /**
* 未登录
*/
public ServerAuthenticationEntryPoint authenticationEntryPoint() {
return (ServerWebExchange exchange, AuthenticationException ex) -> {
Mono<Void> mono = Mono.defer(() -> Mono.just(exchange.getResponse())).flatMap(resp -> ResponseUtils.writeErrorInfo(resp, HttpStatus.FORBIDDEN));
return mono;
};
} /**
* 测试本地公钥(可选)
*/
@SneakyThrows
@Bean
public RSAPublicKey rsaPublicKey() {
Resource resource = new ClassPathResource("public.key");
InputStream is = resource.getInputStream();
String publicKeyData = IoUtil.read(is).toString();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec((Base64.decode(publicKeyData))); KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKey rsaPublicKey = (RSAPublicKey)keyFactory.generatePublic(keySpec);
return rsaPublicKey;
}
}

说明:

  • 公钥可以从远程获取,也可以放在本地从本地读取。上面代码中,被注释调的就是测试一下从本地读取公钥。

从源码中我们也可以看出有多种方式,本例中采用的是从远程获取,因此在前面application.yml中配置了spring.security.oauth2.resourceserver.jwt.jwk-set-uri

响应工具类ResponseUtils

package com.soa.supervision.gateway.util;

import com.alibaba.fastjson.JSON;
import com.tgf.common.domain.RespResult;
import com.tgf.common.util.RespUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; /**
* @Author ChengJianSheng
* @Date 2022/2/16
*/
public class ResponseUtils {
public static Mono<Void> writeErrorInfo(ServerHttpResponse response, HttpStatus httpStatus) {
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.getHeaders().set("Access-Control-Allow-Origin", "*");
response.getHeaders().set("Cache-Control", "no-cache"); RespResult respResult = RespUtils.fail(httpStatus.value(), httpStatus.getReasonPhrase());
String body = JSON.toJSONString(respResult);
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(buffer))
.doOnError(error -> DataBufferUtils.release(buffer));
}
}

鉴权通过以后,可以解析token,并将一些有用的信息放到header中传给下游的业务服务,这样的话业务服务就无需再解析token了,在网关这里统一处理是最适合的了

TokenFilter

package com.soa.supervision.gateway.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.nimbusds.jose.JWSObject;
import com.soa.supervision.gateway.constant.AuthConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import java.text.ParseException; /**
* 只有当请求URL匹配路由规则时才会执行全局过滤器
*
* @Author ChengJianSheng
* @Date 2021/12/15
*/
@Slf4j
@Component
public class TokenFilter implements GlobalFilter { @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String token = request.getHeaders().getFirst(AuthConstants.JWT_TOKEN_HEADER); if (StringUtils.isBlank(token)) {
return chain.filter(exchange);
} String realToken = token.trim().substring(7); try {
JWSObject jwsObject = JWSObject.parse(realToken);
String payload = jwsObject.getPayload().toString();
JSONObject jsonObject = JSON.parseObject(payload);
String userId = jsonObject.getString("userId");
String deptId = jsonObject.getString("deptId");
request = request.mutate()
.header(AuthConstants.HEADER_USER_ID, userId)
.header(AuthConstants.HEADER_DEPT_ID, deptId)
.build();
// 可以把整个Payload放到请求头中
// exchange.getRequest().mutate().header("user", payload).build();
exchange = exchange.mutate().request(request).build();
} catch (ParseException e) {
log.error("解析token失败!原因: {}", e.getMessage(), e);
} return chain.filter(exchange);
}
}

最后,是几个常量类

AuthConstants

package com.soa.supervision.gateway.constant;

/**
* @Author ChengJianSheng
* @Date 2021/11/17
*/
public class AuthConstants { public static final String ROLE_PREFIX = "ROLE_";
public static final String JWT_TOKEN_HEADER = "Authorization";
public static final String JWT_TOKEN_PREFIX = "Bearer "; public static final String TOKEN_WHITELIST_PREFIX = "TOKEN:"; public static final String HEADER_USER_ID = "x-user-id";
public static final String HEADER_DEPT_ID = "x-dept-id";
}

RedisConstants

package com.soa.supervision.gateway.constant;

/**
* @Author ChengJianSheng
* @Date 2022/2/16
*/
public class RedisConstants {
// 资源角色映射关系
public static final String URL_ROLE_MAP_HK = "URL_ROLE_HS";
// 有效的TOKEN
public static final String ONLINE_TOKEN_PREFIX_KV = "ONLINE_TOKEN:";
}

最后,数据库脚本

DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`system_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '系统名称',
`system_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '系统编码',
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称',
`parent_id` int(11) NOT NULL COMMENT '父级菜单ID',
`route_path` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '路由地址',
`component` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组件',
`icon` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标',
`sort` smallint(8) NOT NULL COMMENT '排序号',
`hidden` tinyint(4) NOT NULL COMMENT '是否隐藏(1:是,0:否)',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`create_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
`update_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '修改人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单表' ROW_FORMAT = DYNAMIC; DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`menu_id` int(11) NOT NULL COMMENT '菜单ID',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '名称',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'URL',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '权限表' ROW_FORMAT = Dynamic; DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名称',
`code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色编码',
`sort` smallint(8) NOT NULL COMMENT '排序号',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '修改时间',
`create_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人',
`update_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色表' ROW_FORMAT = DYNAMIC; DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` int(11) NOT NULL COMMENT '角色ID',
`menu_id` int(11) NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色菜单表' ROW_FORMAT = DYNAMIC;

项目截图

5  有用的文档

https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide

https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/html5/

https://docs.spring.io/spring-security/reference/index.html

https://github.com/spring-projects/spring-security-samples/tree/5.6.x

https://github.com/spring-projects/spring-security/wiki</font

https://jwt.io/

https://jwt.io/introduction

Spring Security实现统一登录与权限控制的更多相关文章

  1. spring boot系列--spring security (基于数据库)登录和权限控制

    先说一下AuthConfig.java Spring Security的主要配置文件之一 AuthConfig 1 @Configuration 2 @EnableWebSecurity 3 publ ...

  2. spring boot系列03--spring security (基于数据库)登录和权限控制(下)

    (接上篇) 后台 先说一下AuthConfig.java Spring Security的主要配置文件之一 AuthConfig 1 @Configuration 2 @EnableWebSecuri ...

  3. spring boot系列03--spring security (基于数据库)登录和权限控制(上)

    这篇打算写一下登陆权限验证相关 说起来也都是泪,之前涉及权限的比较少所以这次准备起来就比较困难. 踩了好几个大坑,还好最终都一一消化掉(这是废话你没解决你写个什么劲

  4. Spring Boot+Spring Security+JWT 实现 RESTful Api 权限控制

    摘要:用spring-boot开发RESTful API非常的方便,在生产环境中,对发布的API增加授权保护是非常必要的.现在我们来看如何利用JWT技术为API增加授权保护,保证只有获得授权的用户才能 ...

  5. 基于Spring Security 的JSaaS应用的权限管理

    1. 概述 权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源.资源包括访问的页面,访问的数据等,这在传统的应用系统中比较常见.本文介绍的则是基于Saas系统 ...

  6. spring security使用自定义登录界面后,不能返回到之前的请求界面的问题

    昨天因为集成spring security oauth2,所以对之前spring security的配置进行了一些修改,然后就导致登录后不能正确跳转回被拦截的页面,而是返回到localhost根目录. ...

  7. 为什么Spring Security看不见登录失败或者注销的提示

    有很多人在利用Spring Security进行角色权限设计开发时,一般发现正常登录时没问题,但是注销.或者用户名时,直接就回到登录页面了,在登录页面上看不见任何提示信息,如“用户名/密码有误”或“注 ...

  8. 实战开发,使用 Spring Session 与 Spring security 完成网站登录改造!!

    上次小黑在文章中介绍了四种分布式一致性 Session 的实现方式,在这四种中最常用的就是后端集中存储方案,这样即使 web 应用重启或者扩容,Session 都没有丢失的风险. 今天我们就使用这种方 ...

  9. SpringBoot Spring Security 核心组件 认证流程 用户权限信息获取详细讲解

    前言 Spring Security 是一个安全框架, 可以简单地认为 Spring Security 是放在用户和 Spring 应用之间的一个安全屏障, 每一个 web 请求都先要经过 Sprin ...

随机推荐

  1. Azure AD Domain Service(二)为域服务中的机器配置 Azure File Share 磁盘共享

    一,引言 Azure File Share 是支持两种认证方式的! 1)Active Directory 2)Storage account key 记得上次分析的 "Azure File ...

  2. 虫师Selenium2+Python_5、自动化测试模型

    P138--模块化驱动测试实例 P142--参数化搜索关键字 from selenium import webdriver search_text = ['python','中文','text'] # ...

  3. Spring系列14:IoC容器的扩展点

    Spring系列14:IoC容器的扩展点 回顾 知识需要成体系地学习,本系列文章前后有关联,建议按照顺序阅读.上一篇我们详细介绍了Spring Bean的生命周期和丰富的扩展点,没有阅读的强烈建议先阅 ...

  4. php 利用 fsockopen GET/POST 提交表单及上传文件

    1.GET get.php <?php$host = 'demo.fdipzone.com';$port = 80;$errno = '';$errstr = '';$timeout = 30; ...

  5. Django中ORM创建表关系

    一:django中ORM创建表关系 ORM创建外键关系 1.表与表之间的关系 1.表与表之间的关系 一对多 一对一 多对多 2.操作目标条件: 图书表 出版社表 作者表 作者详情表 3.外键关联 一对 ...

  6. ssh远程端口转发&&windows系统提权之信息收集&&网安工具分享(部分)

    一.ssh远程端口转发 背景:当我们在渗透过程中,获取到内网的一台仅有内网IP的服务器后,我们可以通过ssh隧道,将内网某个主机的端口进行远程转发 1.网络拓扑图 假设获取的服务器为web服务器,we ...

  7. 三大流行BI分析平台推荐,企业数据化选择工具

    进入大数据时代以来,对于企业来说,海量的数据不仅是财富,也是负担.无论是大型企业还是小型企业,都面临着同样的挑战--如何利用大数据客户体验,有效达到优化生产力的效果.这也是近年来许多企业选择搭建现代大 ...

  8. 详解用OpenCV绘制各类几何图形

    摘要:本文详细介绍了OpenCV绘制几何图形的方法,利用cv2.line().v2.circle().cv2.rectangle().cv2.ellipse().cv2.polylines().cv2 ...

  9. MySQL查看数据库中所有表占用的空间大小

    select TABLE_NAME, concat(truncate(data_length/1024/1024,2),'MB') as data_size, concat(truncate(inde ...

  10. Python之ini配置文件详解

    INI介绍 INI是英文"初始化"(initialization)的缩写,被用来对操作系统或特定程序初始化或进行参数设置.由节(section). 键(key).值(value)构 ...