Spring Security实现统一登录与权限控制的示例代码

目录
  • 项目介绍
  • 统一认证中心
  • 配置授权服务器
  • 配置WebSecurity
  • 登录
  • 菜单
  • 鉴权
  • 资源访问的一些配置
  • 有用的文档

项目介绍

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

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

统一认证中心

这里采用 Spring Security + Spring Security OAuth2OAuth2是一种认证授权的协议,是一种开放的标准。最长用到的是授权码模式和密码模式,在本例中,用这两种模式都可以。首先,引入相关依赖最主要的依赖是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));
    }
    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增强
     */
    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());
    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();
    }
    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实现类

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;
    }
    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;
}

登录

默认的登录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();
    }
     * 退出
    @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("注销成功");
}

补充:授权码模式获取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>
            <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的依赖中包含了nimbus-jose-jwt,只是版本不是最新的而已,这里如果想使用更高版本的nimbus-jose-jwt的话可以重新声明一下 -->
            <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" />

    <!--输出到控制台-->
    <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>

鉴权

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

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) {
        //  获取访问资源所需的角色
        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;
    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())
                .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));
     * 测试本地公钥(可选)
    @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;
}

说明:

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

从源码中我们也可以看出有多种方式,本例中采用的是从远程获取,因此在前面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`  (
  `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 '修改时间',
) 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`  (
  `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 '角色编码',
  `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 '修改人',
) 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 '角色ID',
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色菜单表' ROW_FORMAT = DYNAMIC;

项目截图

有用的文档

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实现统一登录与权限控制的示例代码的文章就介绍到这了,更多相关Spring Security登录权限控制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot如何整合Springsecurity实现数据库登录及权限控制

    目录 第一步 第二步是封装一个自定义的类 第三步, 我们需要判断密码啦 总结 我们今天使用SpringBoot来整合SpringSecurity,来吧,不多BB 首先呢,是一个SpringBoot 项目,连接数据库,这里我使用的是mybaties.mysql, 下面是数据库的表 DROP TABLE IF EXISTS `xy_role`; CREATE TABLE `xy_role` ( `xyr_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键i

  • Springboot+Spring Security实现前后端分离登录认证及权限控制的示例代码

    目录 前言 本文主要的功能 一.准备工作 1.统一错误码枚举 2.统一json返回体 3.返回体构造工具 4.pom 5.配置文件 二.数据库表设计 初始化表数据语句 三.Spring Security核心配置:WebSecurityConfig 四.用户登录认证逻辑:UserDetailsService 1.创建自定义UserDetailsService 2.准备service和dao层方法 五.用户密码加密 六.屏蔽Spring Security默认重定向登录页面以实现前后端分离功能 1.实

  • Spring Security实现统一登录与权限控制的示例代码

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

  • Spring Security使用单点登录的权限功能

    目录 背景 Spring Security 实现 已经有了单点登录页面,Spring Security怎么登录,不登录可以拿到权限吗 Authentication 继续分析 结论 如何手动登录Spring Security 结论 总结 附 参考 背景 在配置中心增加权限功能 目前配置中心已经包含了单点登录功能,可以通过统一页面进行登录,登录完会将用户写入用户表 RBAC的用户.角色.权限表CRUD.授权等都已经完成 希望不用用户再次登录,就可以使用SpringSecurity的权限控制 Spri

  • 详解Spring Security 中的四种权限控制方式

    Spring Security 中对于权限控制默认已经提供了很多了,但是,一个优秀的框架必须具备良好的扩展性,恰好,Spring Security 的扩展性就非常棒,我们既可以使用 Spring Security 提供的方式做授权,也可以自定义授权逻辑.一句话,你想怎么玩都可以! 今天松哥来和大家介绍一下 Spring Security 中四种常见的权限控制方式. 表达式控制 URL 路径权限 表达式控制方法权限 使用过滤注解 动态权限 四种方式,我们分别来看.  1.表达式控制 URL 路径权

  • Springboot+Vue+shiro实现前后端分离、权限控制的示例代码

    本文总结自实习中对项目的重构.原先项目采用Springboot+freemarker模版,开发过程中觉得前端逻辑写的实在恶心,后端Controller层还必须返回Freemarker模版的ModelAndView,逐渐有了前后端分离的想法,由于之前,没有接触过,主要参考的还是网上的一些博客教程等,初步完成了前后端分离,在此记录以备查阅. 一.前后端分离思想 前端从后端剥离,形成一个前端工程,前端只利用Json来和后端进行交互,后端不返回页面,只返回Json数据.前后端之间完全通过public A

  • vue实现菜单权限控制的示例代码

    大家在做后台管理系统时一般都会涉及到菜单的权限控制问题.当然解决问题的方法无非两种--前端控制和后端控制.我们公司这边的产品迭代速度较快,所以我们是从前端控制路由迭代到后端控制路由.下面我会分别介绍这两种方法的优缺点以及如何实现(不熟悉vue-router API的同学可以先去官网看一波API哈). 我先简单说下项目的需求:如下图所示,有一级菜单和二级菜单,然后不同的人登录进去会展示不同的菜单. 前端控制路由的思路:将所有的路由映射表都拿到前端来维护,就是我的router.js里面将所有的菜单p

  • Vue 实现前端权限控制的示例代码

    登录&&权限流程图 前言 首先我们确定的权限控制分为三大部分,其中根据粒度大小分的更细: 登录权限控制 页面权限控制 菜单中的页面是否可以被访问 页面中的按钮 (增.删.改.查)的权限控制是否显示 接口权限控制 一.登录权限控制 登录访问权限控制是对用户的校验.在用户登录成功之后,后台将返回一个token,之后前端每次进行接口请求的时候,都要带上这个token.后台拿到这个token后进行判断,如果此token确实存在并且没有过期,则可以通过访问.如果token不存在或后台判断已过期,则会

  • 基于Vue实现后台系统权限控制的示例代码

    用Vue/React这类双向绑定框架做后台系统再适合不过,后台系统相比普通前端项目除了数据交互更频繁以外,还有一个特别的需求就是对用户的权限控制,那么如何在一个Vue应用中实现权限控制呢?下面是我的一点经验. 权限控制是什么 在权限的世界里服务端提供的一切都是资源,资源可以由请求方法+请求地址来描述,权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源.具体的说,前端对资源的访问通常是由界面上的按钮发起,比如删除某条数据:或由用户进入某一个页面发起,比如获取某个列表数据

  • SpringBoot+Spring Security+JWT实现RESTful Api权限控制的方法

    摘要:用spring-boot开发RESTful API非常的方便,在生产环境中,对发布的API增加授权保护是非常必要的.现在我们来看如何利用JWT技术为API增加授权保护,保证只有获得授权的用户才能够访问API. 一:开发一个简单的API 在IDEA开发工具中新建一个maven工程,添加对应的依赖如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-b

  • vue-router 权限控制的示例代码

    最近搭建了公司的后台管理系统, 而且系统还比较庞大, 要实现以下几点: 菜单权限, 根据不同权限显示不同的菜单 操作权限, 比如有些账号没有新增权限, 有些没有修改或者删除权限 数据权限, 比如统计概况, 普通管理员不能看到公司营业概况,但能看到自己所属区域的概况 显示权限, 比如列表, 运营能看到那一列的签约金额,但市场不能看到签约金额这一列 到目前为止还在构建中, 已经解决菜单权限和操作权限. 菜单权限: 最开始的时候也是按照楼上给出的方法,本地先配置一套路由,然后登陆成功后会从服务器返回一

随机推荐