Spring?Security實(shí)現(xiàn)統(tǒng)一登錄與權(quán)限控制的示例代碼
項(xiàng)目介紹
最開(kāi)始是一個(gè)單體應(yīng)用,所有功能模塊都寫(xiě)在一個(gè)項(xiàng)目里,后來(lái)覺(jué)得項(xiàng)目越來(lái)越大,于是決定把一些功能拆分出去,形成一個(gè)一個(gè)獨(dú)立的微服務(wù),于是就有個(gè)問(wèn)題了,登錄、退出、權(quán)限控制這些東西怎么辦呢?總不能每個(gè)服務(wù)都復(fù)制一套吧,最好的方式是將認(rèn)證與鑒權(quán)也單獨(dú)抽離出來(lái)作為公共的服務(wù),業(yè)務(wù)系統(tǒng)只專(zhuān)心做業(yè)務(wù)接口開(kāi)發(fā)即可,完全不用理會(huì)權(quán)限這些與之不相關(guān)的東西了。于是,便有了下面的架構(gòu)圖:

下面重點(diǎn)看一下統(tǒng)一認(rèn)證中心和業(yè)務(wù)網(wǎng)關(guān)的建設(shè)
統(tǒng)一認(rèn)證中心
這里采用 Spring Security + Spring Security OAuth2OAuth2是一種認(rèn)證授權(quán)的協(xié)議,是一種開(kāi)放的標(biāo)準(zhǔn)。最長(zhǎng)用到的是授權(quán)碼模式和密碼模式,在本例中,用這兩種模式都可以。首先,引入相關(guān)依賴(lài)最主要的依賴(lài)是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>配置授權(quán)服務(wù)器
在授權(quán)服務(wù)器中,主要是配置如何生成Token,以及注冊(cè)的客戶(hù)端有哪些
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;
/**
* 授權(quán)服務(wù)器配置
* 1、配置客戶(hù)端
* 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));
}
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
// security.tokenKeyAccess("permitAll()");
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增強(qiáng)
*/
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加密算法對(duì)JWT進(jìn)行簽名
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair());
return jwtAccessTokenConverter;
* 密鑰對(duì)
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
public TokenKeyEndpoint tokenKeyEndpoint() {
return new TokenKeyEndpoint(jwtAccessTokenConverter());
}說(shuō)明:
- 客戶(hù)端是從數(shù)據(jù)庫(kù)加載的
- 密碼模式下必須設(shè)置一個(gè)AuthenticationManager
- 采用JWT生成token是因?yàn)樗p量級(jí),無(wú)需存儲(chǔ)可以減小服務(wù)端的存儲(chǔ)壓力。但是,為了實(shí)現(xiàn)退出功能,不得不將它存儲(chǔ)到Redis中
- 必須要對(duì)JWT進(jìn)行加密,資源服務(wù)器在拿到客戶(hù)端傳的token時(shí)會(huì)去校驗(yàn)該token是否合法,否則客戶(hù)端可能偽造token
- 此處對(duì)token進(jìn)行了增強(qiáng),在token中加了幾個(gè)字段分別表示用戶(hù)ID和部門(mén)ID
客戶(hù)端表結(jié)構(gòu)如下:
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 '客戶(hù)端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 '客戶(hù)端密鑰',
`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 '授權(quán)類(lèi)型',
`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的有效時(shí)間',
`refresh_token_validity` int(11) NULL DEFAULT NULL COMMENT 'refresh_token的有效時(shí)間',
`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 '是否允許自動(dòng)授權(quán)',
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非對(duì)稱(chēng)加密,密鑰文件用的是java自帶的keytools生成的

將來(lái),認(rèn)證服務(wù)器用私鑰對(duì)token加密,然后將公鑰公開(kāi)
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中主要是配置用戶(hù),以及哪些請(qǐng)求需要認(rèn)證以后才能訪問(wèn)
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();
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}UserDetailsService實(shí)現(xiàn)類(lèi)
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("用戶(hù)不存在");
}
if (!authUserDTO.isEnabled()) {
throw new LockedException("賬號(hào)被禁用");
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>
* 用戶(hù)表 服務(wù)類(lèi)
* </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>
* 用戶(hù)表 服務(wù)實(shí)現(xiàn)類(lèi)
* </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;
/**
* 用戶(hù)表 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>
<!-- 根據(jù)用戶(hù)名查用戶(hù) -->
<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 {
/**
* 擴(kuò)展字段
*/
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;
}
public String getPassword() {
return password;
public String getUsername() {
return username;
public boolean isAccountNonExpired() {
return true;
public boolean isAccountNonLocked() {
public boolean isCredentialsNonExpired() {
public boolean isEnabled() {
return enabled;
public Integer getUserId() {
return userId;
public Integer getDeptId() {
return deptId;
}登錄
默認(rèn)的登錄url是/login,本例中沒(méi)有自定義登錄頁(yè)面,而是使用默認(rèn)的登錄頁(yè)面
正常的密碼模式下,輸入用戶(hù)名和密碼,登錄成功以后返回token。本例中使用密碼模式,所以寫(xiě)了個(gè)登錄接口,而且也是取巧,覆蓋了默認(rèn)的/oauth/token端點(diǎn)
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();
}
* 退出
@PostMapping("/logout")
public RespResult logout() {
// JSONObject payload = JwtUtils.getJwtPayload();
// String jti = payload.getStr(SecurityConstants.JWT_JTI); // JWT唯一標(biāo)識(shí)
// Long expireTime = payload.getLong(SecurityConstants.JWT_EXP); // JWT過(guò)期時(shí)間戳(單位:秒)
// if (expireTime != null) {
// long currentTime = System.currentTimeMillis() / 1000;// 當(dāng)前時(shí)間(單位:秒)
// if (expireTime > currentTime) { // token未過(guò)期,添加至緩存作為黑名單限制訪問(wèn),緩存時(shí)間為token過(guò)期剩余時(shí)間
// redisTemplate.opsForValue().set(SecurityConstants.TOKEN_BLACKLIST_PREFIX + jti, null, (expireTime - currentTime), TimeUnit.SECONDS);
// }
// } else { // token 永不過(guò)期則永久加入黑名單
// redisTemplate.opsForValue().set(SecurityConstants.TOKEN_BLACKLIST_PREFIX + jti, null);
// }
// return Result.success("注銷(xiāo)成功");
}補(bǔ)充:授權(quán)碼模式獲取access_token

菜單
登錄以后,前端會(huì)查詢(xún)菜單并展示,下面是菜單相關(guān)接口
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>
* 菜單表 服務(wù)類(lèi)
* </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>
* 菜單表 服務(wù)實(shí)現(xiàn)類(lèi)
* </p>
*
* @author ChengJianSheng
* @since 2022-02-21
*/
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
@Autowired
private SysMenuMapper sysMenuMapper;
/**
* 構(gòu)造菜單樹(shù)
* @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;
/**
* 菜單名稱(chēng)
*/
private String name;
* 父級(jí)菜單ID
private Integer parentId;
* 路由地址
private String routePath;
* 組件
private String component;
* 圖標(biāo)
private String icon;
* 排序號(hào)
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">
<!-- 根據(jù)角色查菜單 -->
<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網(wǎng)關(guān)
在這里,網(wǎng)關(guān)相當(dāng)于OAuth2中的資源服務(wù)器這么個(gè)角色。網(wǎng)關(guān)代理了所有的業(yè)務(wù)微服務(wù),如果說(shuō)那些業(yè)務(wù)服務(wù)是資源的,那么網(wǎng)關(guān)就是資源的集合,訪問(wèn)網(wǎng)關(guān)就是訪問(wèn)資源,訪問(wèn)資源就要先認(rèn)證再授權(quán)才能訪問(wèn)。同時(shí),網(wǎng)關(guān)又相當(dāng)于一個(gè)公共方法,因此在這里做鑒權(quán)是比較合適的。
首先是依賴(lài)
<?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>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security.version}</version>
<artifactId>spring-security-oauth2-resource-server</artifactId>
<artifactId>spring-security-oauth2-jose</artifactId>
<!-- spring-security-oauth2-jose的依賴(lài)中包含了nimbus-jose-jwt,只是版本不是最新的而已,這里如果想使用更高版本的nimbus-jose-jwt的話(huà)可以重新聲明一下 -->
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.15.2</version>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.21</version>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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
- Path=/soas-portal/**
- id: soas-finance
uri: http://127.0.0.1:8095
- 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" />
<!--輸出到控制臺(tái)-->
<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>
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="file" />
</root>
</configuration>鑒權(quán)
真正的權(quán)限判斷或者說(shuō)權(quán)限控制是在這里,下面這段代碼尤為重要,而且它在整個(gè)網(wǎng)關(guān)過(guò)濾器之前調(diào)用
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) {
// 獲取訪問(wèn)資源所需的角色
List<String> authorizedRoles = new ArrayList<>(); // 擁有訪問(wèn)權(quán)限的角色
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);
}
// 沒(méi)有配置權(quán)限規(guī)則表示無(wú)需授權(quán),直接放行
if (CollectionUtils.isEmpty(authorizedRoles)) {
return Mono.just(new AuthorizationDecision(true));
// 判斷用戶(hù)擁有的角色是否可以訪問(wèn)資源
return authentication.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority).any(authorizedRoles::contains)
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
}
}菜單權(quán)限在Redis中是這樣存儲(chǔ)的
url -> [角色編碼, 角色編碼, 角色編碼]

查詢(xún)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;
存儲(chǔ)到Redis
HSET "/soas-order/order/pageList" "[\"admin\",\"org\"]" HSET "/soas-order/order/save" "[\"admin\",\"enterprise\"]"
資源訪問(wèn)的一些配置
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;
private AuthorizationManager authorizationManager;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// 配置JWT解碼相關(guān)
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());//.publicKey(rsaPublicKey());
http.authorizeExchange()
.pathMatchers(ignoreUrlProperties.getUrls()).permitAll()
.anyExchange().access(authorizationManager)
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.authenticationEntryPoint(authenticationEntryPoint())
.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);
/**
* 未授權(quán)(沒(méi)有訪問(wèn)權(quán)限)
*/
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));
* 測(cè)試本地公鑰(可選)
@SneakyThrows
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;
}說(shuō)明:
公鑰可以從遠(yuǎn)程獲取,也可以放在本地從本地讀取。上面代碼中,被注釋調(diào)的就是測(cè)試一下從本地讀取公鑰。
從源碼中我們也可以看出有多種方式,本例中采用的是從遠(yuǎn)程獲取,因此在前面application.yml中配置了spring.security.oauth2.resourceserver.jwt.jwk-set-uri

響應(yīng)工具類(lèi)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));
}
}鑒權(quán)通過(guò)以后,可以解析token,并將一些有用的信息放到header中傳給下游的業(yè)務(wù)服務(wù),這樣的話(huà)業(yè)務(wù)服務(wù)就無(wú)需再解析token了,在網(wǎng)關(guān)這里統(tǒng)一處理是最適合的了
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;
/**
* 只有當(dāng)請(qǐng)求URL匹配路由規(guī)則時(shí)才會(huì)執(zhí)行全局過(guò)濾器
*
* @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();
// 可以把整個(gè)Payload放到請(qǐng)求頭中
// 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);
}
}最后,是幾個(gè)常量類(lèi)
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 {
// 資源角色映射關(guān)系
public static final String URL_ROLE_MAP_HK = "URL_ROLE_HS";
// 有效的TOKEN
public static final String ONLINE_TOKEN_PREFIX_KV = "ONLINE_TOKEN:";
}
最后,數(shù)據(jù)庫(kù)腳本
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 '系統(tǒng)名稱(chēng)', `system_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '系統(tǒng)編碼', `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜單名稱(chēng)', `parent_id` int(11) NOT NULL COMMENT '父級(jí)菜單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 '圖標(biāo)', `sort` smallint(8) NOT NULL COMMENT '排序號(hào)', `hidden` tinyint(4) NOT NULL COMMENT '是否隱藏(1:是,0:否)', `create_time` datetime NOT NULL COMMENT '創(chuàng)建時(shí)間', `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時(shí)間', `create_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '創(chuàng)建人', `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` ( `menu_id` int(11) NOT NULL COMMENT '菜單ID', `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '名稱(chēng)', `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'URL', `create_time` datetime NULL DEFAULT NULL COMMENT '創(chuàng)建時(shí)間', `update_time` datetime NULL DEFAULT NULL COMMENT '修改時(shí)間', ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '權(quán)限表' ROW_FORMAT = Dynamic; DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名稱(chēng)', `code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色編碼', `update_time` datetime NOT NULL COMMENT '修改時(shí)間', `create_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '創(chuàng)建人', `update_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改人', ) 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` ( `role_id` int(11) NOT NULL COMMENT '角色I(xiàn)D', ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色菜單表' ROW_FORMAT = DYNAMIC;
項(xiàng)目截圖

有用的文檔
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
到此這篇關(guān)于Spring Security實(shí)現(xiàn)統(tǒng)一登錄與權(quán)限控制的示例代碼的文章就介紹到這了,更多相關(guān)Spring Security登錄權(quán)限控制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合Security權(quán)限控制登錄首頁(yè)
- SpringBoot?整合Security權(quán)限控制的初步配置
- 詳解如何在項(xiàng)目中應(yīng)用SpringSecurity權(quán)限控制
- Spring?Security權(quán)限管理實(shí)現(xiàn)接口動(dòng)態(tài)權(quán)限控制
- 基于Spring Security前后端分離的權(quán)限控制系統(tǒng)問(wèn)題
- 詳解Spring Security 中的四種權(quán)限控制方式
- SpringSecurity權(quán)限控制實(shí)現(xiàn)原理解析
- Spring?Security權(quán)限控制的實(shí)現(xiàn)接口
相關(guān)文章
Spring Boot 與 kotlin 使用Thymeleaf模板引擎渲染web視圖的方法
這篇文章主要介紹了Spring Boot 與 kotlin 使用Thymeleaf模板引擎渲染web視圖的方法,本文給大家介紹的非常詳細(xì),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-01-01
淺談Java中的interface應(yīng)用與面向接口編程
這篇文章主要介紹了淺談Java中的interface應(yīng)用與面向接口編程,Java的關(guān)鍵字interface應(yīng)用,一個(gè)接口,多個(gè)實(shí)現(xiàn)類(lèi),面向接口編程,把業(yè)務(wù)邏輯線(xiàn)提取出來(lái)作為接口,具體的業(yè)務(wù)實(shí)現(xiàn)通過(guò)該接口的實(shí)現(xiàn)類(lèi)來(lái)完成,需要的朋友可以參考下2023-10-10
Java?C++題解leetcode902最大為N的數(shù)字組合數(shù)位DP
這篇文章主要為大家介紹了Java?C++題解leetcode902最大為N的數(shù)字組合數(shù)位DP,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
Spring cloud Feign 深度學(xué)習(xí)與應(yīng)用詳解
這篇文章主要介紹了Spring cloud Feign 深度學(xué)習(xí)與應(yīng)用詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-06-06
詳解如何使用tldb數(shù)據(jù)庫(kù)的java客戶(hù)端
這篇文章主要為大家介紹了如何使用tldb數(shù)據(jù)庫(kù)的java客戶(hù)端過(guò)程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09

