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

1 项目介绍

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

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

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

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 -->
    </relativepath></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;

/**
 * &#x6388;&#x6743;&#x670D;&#x52A1;&#x5668;&#x914D;&#x7F6E;
 * 1&#x3001;&#x914D;&#x7F6E;&#x5BA2;&#x6237;&#x7AEF;
 * 2&#x3001;&#x914D;&#x7F6E;Access_Token&#x751F;&#x6210;
 *
 * @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 tokenEnhancerList = new ArrayList<>();
        tokenEnhancerList.add(jwtTokenEnhancer());
        tokenEnhancerList.add(jwtAccessTokenConverter());
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancerList);
        endpoints.accessTokenConverter(jwtAccessTokenConverter()).tokenEnhancer(tokenEnhancerChain).authenticationManager(authenticationManager);
    }

    /**
     * Token&#x589E;&#x5F3A;
     */
    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;
            }
        };
    }

    /**
     * &#x91C7;&#x7528;RSA&#x52A0;&#x5BC6;&#x7B97;&#x6CD5;&#x5BF9;JWT&#x8FDB;&#x884C;&#x7B7E;&#x540D;
     */
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setKeyPair(keyPair());
        return jwtAccessTokenConverter;
    }

    /**
     * &#x5BC6;&#x94A5;&#x5BF9;
     */
    @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());
    }
}</string,>

说明:

  • 客户端是从数据库加载的
  • 密码模式下必须设置一个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 '&#x5BA2;&#x6237;&#x7AEF;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 '&#x5BA2;&#x6237;&#x7AEF;&#x5BC6;&#x94A5;',
    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 '&#x6388;&#x6743;&#x7C7B;&#x578B;',
    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&#x7684;&#x6709;&#x6548;&#x65F6;&#x95F4;',
    refresh_token_validity  int(11) NULL DEFAULT NULL COMMENT 'refresh_token&#x7684;&#x6709;&#x6548;&#x65F6;&#x95F4;',
    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 '&#x662F;&#x5426;&#x5141;&#x8BB8;&#x81EA;&#x52A8;&#x6388;&#x6743;',
    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生成的

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

将来,认证服务器用私钥对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();
    }
}</string,>

配置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("&#x7528;&#x6237;&#x4E0D;&#x5B58;&#x5728;");
        }
        if (!authUserDTO.isEnabled()) {
            throw new LockedException("&#x8D26;&#x53F7;&#x88AB;&#x7981;&#x7528;");
        }
        Set 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;

/**
 * &#x7528;&#x6237;&#x8868; &#x670D;&#x52A1;&#x7C7B;
 *
 * @author ChengJianSheng
 * @since 2022-02-14
 */
public interface SysUserService extends IService {
    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 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;

/**
 * &#x7528;&#x6237;&#x8868; &#x670D;&#x52A1;&#x5B9E;&#x73B0;&#x7C7B;
 *
 * @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,>

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;

/**
 * &#x7528;&#x6237;&#x8868; Mapper &#x63A5;&#x53E3;
 *
 * @author ChengJianSheng
 * @since 2022-02-14
 */
public interface SysUserMapper extends BaseMapper {
    AuthUserDTO selectAuthUserByUsername(String username);
}

SysUserMapper.xml

<!-- 根据用户名查用户 -->
<select id="selectAuthUserByUsername">
    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>

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 {
    /**
     * &#x6269;&#x5C55;&#x5B57;&#x6BB5;
     */
    private Integer userId;
    private Integer deptId;
    private String username;
    private String password;
    private boolean enabled;
    private Set 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;

    /**
     * &#x5BC6;&#x7801;&#x6A21;&#x5F0F; &#x767B;&#x5F55;
     *
     * @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<>(); // &#x7F13;&#x5B58;
        return RespUtils.success();
    }

    /**
     * &#x9000;&#x51FA; * @return
     */
    @PostMapping("/logout")
    public RespResult logout() {
//        JSONObject payload = JwtUtils.getJwtPayload();
//        String jti = payload.getStr(SecurityConstants.JWT_JTI);         // JWT&#x552F;&#x4E00;&#x6807;&#x8BC6;
//        Long expireTime = payload.getLong(SecurityConstants.JWT_EXP);   // JWT&#x8FC7;&#x671F;&#x65F6;&#x95F4;&#x6233;(&#x5355;&#x4F4D;&#xFF1A;&#x79D2;)
//        if (expireTime != null) {
//            long currentTime = System.currentTimeMillis() / 1000;       // &#x5F53;&#x524D;&#x65F6;&#x95F4;&#xFF08;&#x5355;&#x4F4D;&#xFF1A;&#x79D2;&#xFF09;
//            if (expireTime > currentTime) {  // token&#x672A;&#x8FC7;&#x671F;&#xFF0C;&#x6DFB;&#x52A0;&#x81F3;&#x7F13;&#x5B58;&#x4F5C;&#x4E3A;&#x9ED1;&#x540D;&#x5355;&#x9650;&#x5236;&#x8BBF;&#x95EE;&#xFF0C;&#x7F13;&#x5B58;&#x65F6;&#x95F4;&#x4E3A;token&#x8FC7;&#x671F;&#x5269;&#x4F59;&#x65F6;&#x95F4;
//                redisTemplate.opsForValue().set(SecurityConstants.TOKEN_BLACKLIST_PREFIX + jti, null, (expireTime - currentTime), TimeUnit.SECONDS);
//            }
//        } else {  // token &#x6C38;&#x4E0D;&#x8FC7;&#x671F;&#x5219;&#x6C38;&#x4E45;&#x52A0;&#x5165;&#x9ED1;&#x540D;&#x5355;
//            redisTemplate.opsForValue().set(SecurityConstants.TOKEN_BLACKLIST_PREFIX + jti, null);
//        }
//        return Result.success("&#x6CE8;&#x9500;&#x6210;&#x529F;");
        return RespUtils.success();
    }
}</string,></string,>

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

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

补充:

授权码模式获取access_token

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

菜单

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

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;

/**
 * &#x83DC;&#x5355;&#x8868; &#x524D;&#x7AEF;&#x63A7;&#x5236;&#x5668;
 * @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 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;

/**
 * &#x83DC;&#x5355;&#x8868; &#x670D;&#x52A1;&#x7C7B;
 * @author ChengJianSheng
 * @since 2022-02-21
 */
public interface SysMenuService extends IService {
    List 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;

/**
 * &#x83DC;&#x5355;&#x8868; &#x670D;&#x52A1;&#x5B9E;&#x73B0;&#x7C7B;
 *
 * @author ChengJianSheng
 * @since 2022-02-21
 */
@Service
public class SysMenuServiceImpl extends ServiceImpl<sysmenumapper, sysmenu> implements SysMenuService {
    @Autowired
    private SysMenuMapper sysMenuMapper;

    /**
     * &#x6784;&#x9020;&#x83DC;&#x5355;&#x6811;
     * @param systemCode
     * @param roleIds
     * @return
     */
    @Override
    public List getMenuByUserId(String systemCode, Integer userId) {
        List voList = new ArrayList<>();
        List sysMenuList = sysMenuMapper.selectMenuByUserId(systemCode, userId);
        if (null == sysMenuList || sysMenuList.size() == 0) {
            return voList;
        }
        List 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());
    }
}</sysmenumapper,>

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;
    /**
     * &#x83DC;&#x5355;&#x540D;&#x79F0;
     */
    private String name;
    /**
     * &#x7236;&#x7EA7;&#x83DC;&#x5355;ID
     */
    private Integer parentId;
    /**
     * &#x8DEF;&#x7531;&#x5730;&#x5740;
     */
    private String routePath;
    /**
     * &#x7EC4;&#x4EF6;
     */
    private String component;
    /**
     * &#x56FE;&#x6807;
     */
    private String icon;
    /**
     * &#x6392;&#x5E8F;&#x53F7;
     */
    private Integer sort;
    /**
     * &#x5B50;&#x83DC;&#x5355;
     */
    private List 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;

/**
 * &#x83DC;&#x5355;&#x8868; Mapper &#x63A5;&#x53E3;
 * @author ChengJianSheng
 * @since 2022-02-21
 */
public interface SysMenuMapper extends BaseMapper {
    List selectMenuByUserId(@Param("systemCode") String systemCode, @Param("userId") Integer userId);
}

SysMenuMapper.xml

<!-- 根据用户查菜单 -->
<select id="selectMenuByUserId">
    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>

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

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

鉴权

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

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 {
    private final PathMatcher pathMatcher = new AntPathMatcher();
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Mono check(Mono authentication, AuthorizationContext context) {
        ServerHttpRequest request = context.getExchange().getRequest();
        String path = request.getURI().getPath();
        // token&#x4E0D;&#x80FD;&#x4E3A;&#x7A7A;&#x4E14;&#x6709;&#x6548;
        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 authorizedroles="new" arraylist<>();
        // &#x62E5;&#x6709;&#x8BBF;&#x95EE;&#x6743;&#x9650;&#x7684;&#x89D2;&#x8272;
        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 roles = JSON.parseArray((String) entry.getValue(), String.class);
            if (pathMatcher.match(permissionUrl, path)) {
                authorizedRoles.addAll(roles);
            }
        }
        // &#x6CA1;&#x6709;&#x914D;&#x7F6E;&#x6743;&#x9650;&#x89C4;&#x5219;&#x8868;&#x793A;&#x65E0;&#x9700;&#x6388;&#x6743;&#xFF0C;&#x76F4;&#x63A5;&#x653E;&#x884C;
        if (CollectionUtils.isEmpty(authorizedRoles)) {
            return Mono.just(new AuthorizationDecision(true));
        }
        // &#x5224;&#x65AD;&#x7528;&#x6237;&#x62E5;&#x6709;&#x7684;&#x89D2;&#x8272;&#x662F;&#x5426;&#x53EF;&#x4EE5;&#x8BBF;&#x95EE;&#x8D44;&#x6E90;
        return authentication
                .filter(Authentication::isAuthenticated)
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
                .any(authorizedRoles::contains)
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
    }
}</object,></object,></=>

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

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

查询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) {
        // &#x914D;&#x7F6E;JWT&#x89E3;&#x7801;&#x76F8;&#x5173;
        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> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        // jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
    }

    /**
     * &#x672A;&#x6388;&#x6743;&#xFF08;&#x6CA1;&#x6709;&#x8BBF;&#x95EE;&#x6743;&#x9650;&#xFF09;
     */
    public ServerAccessDeniedHandler accessDeniedHandler() {
        return (ServerWebExchange exchange, AccessDeniedException denied) -> {
            Mono mono = Mono.defer(() -> Mono.just(exchange.getResponse())).flatMap(resp -> ResponseUtils.writeErrorInfo(resp, HttpStatus.UNAUTHORIZED));
            return mono;
        };
    }

    /**
     * &#x672A;&#x767B;&#x5F55;
     */
    public ServerAuthenticationEntryPoint authenticationEntryPoint() {
        return (ServerWebExchange exchange, AuthenticationException ex) -> {
            Mono mono = Mono.defer(() -> Mono.just(exchange.getResponse())).flatMap(resp -> ResponseUtils.writeErrorInfo(resp, HttpStatus.FORBIDDEN));
            return mono;
        };
    }

    /**
     * &#x6D4B;&#x8BD5;&#x672C;&#x5730;&#x516C;&#x94A5;&#xFF08;&#x53EF;&#x9009;&#xFF09;
     */
    @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;
    }
}</jwt,>

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

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

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

响应工具类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 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;

/**
 * &#x53EA;&#x6709;&#x5F53;&#x8BF7;&#x6C42;URL&#x5339;&#x914D;&#x8DEF;&#x7531;&#x89C4;&#x5219;&#x65F6;&#x624D;&#x4F1A;&#x6267;&#x884C;&#x5168;&#x5C40;&#x8FC7;&#x6EE4;&#x5668;
 *
 * @Author ChengJianSheng
 * @Date 2021/12/15
 */
@Slf4j
@Component
public class TokenFilter implements GlobalFilter {
    @Override
    public Mono 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(); // &#x53EF;&#x4EE5;&#x628A;&#x6574;&#x4E2A;Payload&#x653E;&#x5230;&#x8BF7;&#x6C42;&#x5934;&#x4E2D;
            exchange.getRequest().mutate().header("user", payload).build();
            exchange = exchange.mutate().request(request).build();
        } catch (ParseException e) {
            log.error("&#x89E3;&#x6790;token&#x5931;&#x8D25;&#xFF01;&#x539F;&#x56E0;: {}", 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 {
    // &#x8D44;&#x6E90;&#x89D2;&#x8272;&#x6620;&#x5C04;&#x5173;&#x7CFB;
    public static final String URL_ROLE_MAP_HK = "URL_ROLE_HS";
    // &#x6709;&#x6548;&#x7684;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 '&#x7CFB;&#x7EDF;&#x540D;&#x79F0;',
    system_code varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci  NOT NULL COMMENT '&#x7CFB;&#x7EDF;&#x7F16;&#x7801;',
    name        varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci  NOT NULL COMMENT '&#x83DC;&#x5355;&#x540D;&#x79F0;',
    parent_id   int(11) NOT NULL COMMENT '&#x7236;&#x7EA7;&#x83DC;&#x5355;ID',
    route_path  varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '&#x8DEF;&#x7531;&#x5730;&#x5740;',
    component   varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '&#x7EC4;&#x4EF6;',
    icon        varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '&#x56FE;&#x6807;',
    sort        smallint(8) NOT NULL COMMENT '&#x6392;&#x5E8F;&#x53F7;',
    hidden      tinyint(4) NOT NULL COMMENT '&#x662F;&#x5426;&#x9690;&#x85CF;&#xFF08;1:&#x662F;&#xFF0C;0:&#x5426;&#xFF09;',
    create_time datetime                                                      NOT NULL COMMENT '&#x521B;&#x5EFA;&#x65F6;&#x95F4;',
    update_time datetime                                                      NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '&#x4FEE;&#x6539;&#x65F6;&#x95F4;',
    create_user varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '&#x521B;&#x5EFA;&#x4EBA;',
    update_user varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '&#x4FEE;&#x6539;&#x4EBA;',
    PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '&#x83DC;&#x5355;&#x8868;' 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 '&#x83DC;&#x5355;ID',
    name        varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '&#x540D;&#x79F0;',
    url         varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'URL',
    create_time datetime NULL DEFAULT NULL COMMENT '&#x521B;&#x5EFA;&#x65F6;&#x95F4;',
    update_time datetime NULL DEFAULT NULL COMMENT '&#x4FEE;&#x6539;&#x65F6;&#x95F4;',
    PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '&#x6743;&#x9650;&#x8868;' 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 '&#x89D2;&#x8272;&#x540D;&#x79F0;',
    code        varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '&#x89D2;&#x8272;&#x7F16;&#x7801;',
    sort        smallint(8) NOT NULL COMMENT '&#x6392;&#x5E8F;&#x53F7;',
    create_time datetime                                                     NOT NULL COMMENT '&#x521B;&#x5EFA;&#x65F6;&#x95F4;',
    update_time datetime                                                     NOT NULL COMMENT '&#x4FEE;&#x6539;&#x65F6;&#x95F4;',
    create_user varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '&#x521B;&#x5EFA;&#x4EBA;',
    update_user varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '&#x4FEE;&#x6539;&#x4EBA;',
    PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '&#x89D2;&#x8272;&#x8868;' 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 '&#x89D2;&#x8272;ID',
    menu_id int(11) NOT NULL COMMENT '&#x83DC;&#x5355;ID',
    PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '&#x89D2;&#x8272;&#x83DC;&#x5355;&#x8868;' ROW_FORMAT = DYNAMIC;

项目截图

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

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

https://jwt.io/

https://jwt.io/introduction

Original: https://www.cnblogs.com/cjsblog/p/16040652.html
Author: 废物大师兄
Title: Spring Security实现统一登录与权限控制

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/612291/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • day01-数据库的安装和使用

    Java数据库的安装和使用 1.数据库的作用 一个问题:淘宝网、京东、微信抖音,都有各自的功能,那么我们退出系统的时候,为什么信息还在? 解决之道-文件,数据库 为了解决上诉问题,…

    数据库 2023年6月11日
    091
  • Docker 部署前后端项目

    Docker 部署前后端项目 平生不会相思,才会相思,便害相思。 简介:都是被逼的,从零开始一个Docker 部署九个微服务和三个前端项目。其中,这些服务需要用到Nacos、MyS…

    数据库 2023年6月14日
    077
  • tomcat加载启动过程

    流程图 posted @2022-08-19 17:43 默念x 阅读(9 ) 评论() 编辑 Original: https://www.cnblogs.com/monianxd…

    数据库 2023年6月16日
    079
  • show engine innodb status 输出结果解读

    show engine innodb status 输出结果解读 基于MySQL 5.7.32最近想整理一下show engine innodb status的解读,但是发现中文互…

    数据库 2023年6月16日
    0101
  • JVM

    JVM 一、什么是JVM 定义 Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境) 好处 一次编写,到处运行 自动内存管理,垃圾回收…

    数据库 2023年6月16日
    051
  • PHP array_count_values()

    array_count_values array_count_values() 函数用于统计数组中所有值出现的次数。 本函数返回一个数组,其元素的键名是原数组的值,键值是该值在原数…

    数据库 2023年6月14日
    066
  • CronExpression使用笔记

    CronExpression一般是使用在自动任务中,可以指定任务执行的时间或者时间规律,下面记录一下表达试的使用说明 CronExpression由7个子表达式组成,7个子表达式之…

    数据库 2023年6月9日
    097
  • 2022-8-20 数据库连接池

    1. 概念:其实就是一个容器(集合),存放数据库连接的容器。 &#x5F53;&#x7CFB;&#x7EDF;&#x521D;&#x59CB…

    数据库 2023年6月14日
    0101
  • git拉项目出现的小问题

    问题描述 在IDEA中拉代码事报错。 点击查看报错信息 error: unable to read askpass response from ‘C:\Users\&#x9…

    数据库 2023年6月11日
    099
  • http状态码总结

    表示临时响应并需要请求者继续执行操作的状态代码。 100 (继续) 请求者应当继续提出请求。 服务器返回此代码表示已收到请求的第一部分,正在等待其余部分。101 (切换协议) 请求…

    数据库 2023年6月6日
    056
  • HA: FORENSICS靶机练习

    ubuntu拿到手,没有恢复模式,不好绕密码,仿真软件又会更改所有用户的密码,怕影响后续操作,先不采用,先试试用john跑一下看看能不能跑出一两个来。 刚好跑出来一个,用户 &lt…

    数据库 2023年6月11日
    077
  • MYSQL(基本篇)——一篇文章带你走进MYSQL的奇妙世界

    MYSQL算是我们程序员必不可少的一份求职工具了 无论在什么岗位,我们都可以看到应聘要求上所书写的”精通MYSQL等数据库及优化” 那么我们今天就先来了解一…

    数据库 2023年5月24日
    086
  • 猴子吃桃(递归)

    递归案例实践分析 猴子偷桃 题目描述: 猴子第一天摘下若干桃子,当即吃了一半,觉得好吃不过瘾,于是又多吃了一个,,第二天又吃了前天剩余桃子数量的一半,觉得好不过瘾,于是又多吃了一个…

    数据库 2023年6月16日
    0147
  • String字符串用逗号拼接,防止最后一位是逗号

    StringBuilder sb = new StringBuilder(); for(String s strArr) { if (sb.length() > 0) {//…

    数据库 2023年6月16日
    094
  • URL解码时,为什么将加号解码为空?

    以下代码在.NET Framework 2.0 中测试。 先看一个例子: test.aspx页面: 当参数 parameters 输出到页面后,值已经不为”A+B&#8…

    数据库 2023年6月11日
    050
  • MySQL中实现中文转拼音

    — 插入数据 INSERT INTO t_base_pinyin ( pin_yin_, code_ ) VALUES ( "a", 20319 ),( &q…

    数据库 2023年6月14日
    090
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球