• Spring Security实现统一登录与权限控制


    1  项目介绍

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

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

    2  统一认证中心

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

    这里Spring Boot的版本是2.6.3
    完整的pom如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>com.tgf</groupId>
            <artifactId>tgf-service-parent</artifactId>
            <version>1.3.0</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.soa.supervision.uaa</groupId>
        <artifactId>soas-uaa</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>soas-uaa</name>
        <properties>
            <java.version>1.8</java.version>
            <spring-cloud.version>2021.0.0</spring-cloud.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-oauth2</artifactId>
                <version>2.2.5.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>com.nimbusds</groupId>
                <artifactId>nimbus-jose-jwt</artifactId>
                <version>9.19</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
                <version>3.5.1</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.scripting</groupId>
                <artifactId>mybatis-freemarker</artifactId>
                <version>1.2.3</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>${spring-cloud.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                            </exclude>
                        </excludes>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    

    配置授权服务器

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

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

    说明:

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

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

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

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

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

    配置WebSecurity

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

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

    UserDetailsService实现类

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

    SysUserService

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

    AuthUserDTO

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

    SysUserServiceImpl

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

    SysUserMapper

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

    SysUserMapper.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.soa.supervision.uaa.mapper.SysUserMapper">
        
        <resultMap id="authUserResultMap" type="com.soa.supervision.uaa.domain.AuthUserDTO">
            <id property="userId" column="id"/>
            <result property="username" column="username"/>
            <result property="password" column="password"/>
            <result property="deptId" column="dept_id"/>
            <result property="enabled" column="enabled"/>
            <collection property="roles" ofType="string" javaType="list">
                <result column="role_code"/>
            </collection>
        </resultMap>
    
        <!-- 根据用户名查用户 -->
        <select id="selectAuthUserByUsername" resultMap="authUserResultMap">
            SELECT
                t1.id,
                t1.username,
                t1.`password`,
                t1.dept_id,
                t1.enabled,
                t3.`code` AS role_code
            FROM
                sys_user t1
                    LEFT JOIN sys_user_role t2 ON t1.id = t2.user_id
                    LEFT JOIN sys_role t3 ON t2.role_id = t3.id
            WHERE
                t1.username = #{username}
        </select>
    
    </mapper>
    

    UserDetails

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

    登录

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

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

    补充:授权码模式获取access_token

    菜单

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

    package com.soa.supervision.uaa.controller;
    
    
    import com.soa.supervision.uaa.domain.MenuVO;
    import com.soa.supervision.uaa.service.SysMenuService;
    import com.tgf.common.domain.RespResult;
    import com.tgf.common.util.RespUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.Arrays;
    import java.util.List;
    
    /**
     * <p>
     * 菜单表 前端控制器
     * </p>
     *
     * @author ChengJianSheng
     * @since 2022-02-21
     */
    @RestController
    @RequestMapping("/menu")
    public class SysMenuController {
    
        @Autowired
        private SysMenuService sysMenuService;
    
        @GetMapping("/tree")
        public RespResult tree(@RequestHeader("userId") Integer userId, String systemCode) {
            if (StringUtils.isBlank(systemCode)) {
                systemCode = "ADMIN";
            }
            List<MenuVO> voList = sysMenuService.getMenuByUserId(systemCode, userId);
            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> getMenuByUserId(String systemCode, Integer userId);
    }
    

    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> getMenuByUserId(String systemCode, Integer userId) {
            List<MenuVO> voList = new ArrayList<>();
    
            List<SysMenu> sysMenuList = sysMenuMapper.selectMenuByUserId(systemCode, userId);
            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> selectMenuByUserId(@Param("systemCode") String systemCode, @Param("userId") Integer userId);
    }
    

    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="selectMenuByUserId" resultType="com.soa.supervision.uaa.entity.SysMenu">
            SELECT
                t1.*
            FROM
                sys_menu t1
                    INNER JOIN sys_role_menu t2 ON t1.id = t2.menu_id
                    INNER JOIN sys_user_role t3 ON t2.role_id = t3.role_id
            WHERE
                t1.type = 1
              AND t1.hidden = 0
              AND t1.system_code = #{systemCode}
              AND t3.user_id = #{userId}
            ORDER BY
                t1.sort ASC
        </select>
    
    </mapper>
    

    application.yml

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

    3  网关

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

    首先是依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>com.tgf</groupId>
            <artifactId>tgf-service-parent</artifactId>
            <version>1.3.1-SNAPSHOT</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.soa.supervision.gateway</groupId>
        <artifactId>soas-gateway</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>soas-gateway</name>
        <properties>
            <java.version>1.8</java.version>
            <spring-security.version>5.6.1</spring-security.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-config</artifactId>
                <version>${spring-security.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-oauth2-resource-server</artifactId>
                <version>${spring-security.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-oauth2-jose</artifactId>
                <version>${spring-security.version}</version>
            </dependency>
            <!-- spring-security-oauth2-jose的依赖中包含了nimbus-jose-jwt,只是版本不是最新的而已,这里如果想使用更高版本的nimbus-jose-jwt的话可以重新声明一下 -->
            <dependency>
                <groupId>com.nimbusds</groupId>
                <artifactId>nimbus-jose-jwt</artifactId>
                <version>9.15.2</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-collections4</artifactId>
            </dependency>
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>5.7.21</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
        </dependencies>
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>${spring-cloud.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                            </exclude>
                        </excludes>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    

    application.yml

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

    直接放行的url

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

    logback.xml

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

    鉴权

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

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

    菜单权限在Redis中是这样存储的
    url -> [角色编码, 角色编码, 角色编码]

    查询SQL

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

    存储到Redis

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

    资源访问的一些配置

    ResourceServerConfig

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

    说明:

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

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

    响应工具类ResponseUtils

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

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

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

    最后,是几个常量类
    AuthConstants

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

    RedisConstants

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

    最后,数据库脚本

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

    项目截图

    5  有用的文档

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

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

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

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

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

    https://jwt.io/

    https://jwt.io/introduction

  • 相关阅读:
    释放下一代网络应用的能量[转载]
    帮助创建未来的 .NET 客户端开发
    ASP.NET Ajax替代品AjaxWidgets
    Microsoft Surface
    有意思的《致招商银行的公开信》行动!
    Applying DomainDriven Design and Patterns(ADDDP) With examples in C# and .NET
    Silverlight ASP.NET control
    StructureMap 轻量IOC框架
    DDay.iCal an iCalendar class library
    Mono ASP.NET 上几个性能调优技巧
  • 原文地址:https://www.cnblogs.com/cjsblog/p/16040652.html
Copyright © 2020-2023  润新知