如何通过Spring Security实现网站统一登录及权限管理功能?

2026-05-22 14:351阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计4905个文字,预计阅读时间需要20分钟。

如何通过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="maven.apache.org/POM/4.0.0" xmlns:xsi="www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="maven.apache.org/POM/4.0.0 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', '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', '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 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

如何通过Spring Security实现网站统一登录及权限管理功能?

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" "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="maven.apache.org/POM/4.0.0" xmlns:xsi="www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="maven.apache.org/POM/4.0.0 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: 127.0.0.1:8093 predicates: - Path=/soas-enterprise/** - id: soas-portal uri: 127.0.0.1:8092 predicates: - Path=/soas-portal/** - id: soas-finance uri: 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: 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.github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide

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

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

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

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

jwt.io/

jwt.io/introduction

本文共计4905个文字,预计阅读时间需要20分钟。

如何通过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="maven.apache.org/POM/4.0.0" xmlns:xsi="www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="maven.apache.org/POM/4.0.0 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', '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', '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 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

如何通过Spring Security实现网站统一登录及权限管理功能?

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" "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="maven.apache.org/POM/4.0.0" xmlns:xsi="www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="maven.apache.org/POM/4.0.0 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: 127.0.0.1:8093 predicates: - Path=/soas-enterprise/** - id: soas-portal uri: 127.0.0.1:8092 predicates: - Path=/soas-portal/** - id: soas-finance uri: 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: 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.github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide

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

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

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

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

jwt.io/

jwt.io/introduction