基于OAuth2+SpringSecurity+Jwt实现身份认证和权限管理后端服务
1、简介
本文讲述了如何实现简易的后端鉴权服务。所谓“鉴权”,就是“身份鉴定”+“权限判断”。涉及的技术有:OAuth2、SpringSecurity、Jwt、过滤器、拦截器。OAuth2用于授权,使用Jwt签发Access Token和Refresh Token,并管理token的过期时间以及刷新校验token。SpringSecurity用于认证,会拿着输入的用户名和密码去数据库中比对,如果比对成功则调用OAuth2取授权签发token。Jwt则被用于生成token,jwt会根据用户信息进行base64编码,并对编码后的字符串进行加密。过滤器则是用在网关,目的是把那些没有认证过的请求,即没有携带token或者携带的token不合法的请求过滤掉,使那些请求不会打到后端其他服务上去。拦截器的作用是在网关身份认证后,请求会被转发到具体的各个后端服务上,如果请求的发起者没有访问接口的权限,那么请求就会被拦截掉。
2、相关技术介绍
2.1、OAuth2
OAuth2是一种授权框架,可以实现第三方授权。OAuth2一共有4种授权模式:
(1)客户端模式:客户端直接向验证服务器请求一个token,获得token后,客户端携带着这个token就能访问相应的其他服务了。不过这种模式下没法进行身份验证。通常适用于服务内部之间调用。类似于feign调用这种。
(2)密码模式:客户端提供用户名密码给验证服务器,用户名和密码验证通过后,验证服务器返回给token,客户端再携带着token去访问其他服务。不过这种模式容易把用户名密码泄露给客户端。比如,你在网站登录页面输入用户名和密码,那么你的用户名和密码就有可能泄露给登录页面。有些钓鱼网站就会以欺骗登录页面的方式获取到用户的用户名和密码。因此使用这种模式需确保客户端是可信的。
(3)隐式授权模式:用户访问某个页面时,如果该用户尚未被身份认证,页面就会重定向到认证服务器,认证服务器会给用户一个认证页面,用户在上面输入用户名和密码完成身份认证后,认证服务器就会返回token。用户就可以拿着token去访问其他服务了。隐式授权模式通常会用于实现sso单点登录。不过该方式会暴露token给用户。
(4)授权码模式:这种模式是最安全的一种模式,也是推荐使用的一种,比如我们手机上的很多 App 都是使用的这种模式。相比隐式授权模式,它并不会直接返回 token,而是返回授权码,真正的 token 是通过应用服务器访问验证服务器获得的。在一开始的时候,应用服务器(客户端通过访问自己的应用服务器来进而访问其他服务)和验证服务器之间会共享一个 secret,这个东西没有其他人知道,而验证服务器在用户验证完成之后,会返回一个授权码,应用服务器最后将授权码和 secret 一起交给验证服务器进行验证,并且 Token 也是在服务端之间传递,是存放在应用服务器上的,不会直接给到客户端。
2.2、SpringSecurity
SpringSecurity是一种安全框架,通常是会集成OAuth2一起使用。
SpringSecurity+OAuth2协作方式:
SpringSecurity可以作为OAuth2授权服务器,验证用户身份的合法性,如果身份合法则让OAuth2签发token。SpringSecurity框架本身也自带了一个登录页面,并且提供了一个WebSecurityConfigurerAdapter类,可以通过继承该类并重载configure方法,实现自定的权限拦截。
简而言之,OAuth2定义了 授权的标准协议,解决“如何安全地允许第三方访问资源”的问题。Spring Security提供了 实现 OAuth2 和安全控制的工具链,包括认证、授权、令牌管理等具体功能。
2.3、JWT
JWT(JSON Web Token),是用于生成token的,其原理是将用户身份信息和声明,编码为紧凑的、自包含的字符串,并通过数字签名保证其完整性和真实性。JWT由Header、Payload、Signature三部分组成:
(1)Header是定义token的元数据,如签名算法和类型(常用的加密算法有SHA256)。并通过base64对Header数据进行编码。
(2)Payload是用于存储用户身份信息和自定义声明。会存储签发者信息、过期时间、签发时间等。也是采用base64编码。
(3)Signature是用于验证token的完整性和真实性,防止篡改。先对 Header 和 Payload 进行 Base64Url 编码,然后再使用密钥(Secret Key)和指定算法(如 HS256、RS256)对编码后的字符串签名。
而最后生成的token就是将三部分用"."拼接起来。即:token=Header.Payload.Signature
2.4、过滤器
过滤器(filter)是java web的核心组件,是用于拦截请求并执行预处理或者后处理逻辑。比较常用的过滤器有Filter和GlobalFilter,Filter是局部过滤器,是java servlet下的组件,仅对特定的路由生效,通常可以在yml里面通过filters关键字进行配置。而GlobalFilter是Spring Cloud Gateway下的组件,是全局过滤器。过滤拦截所有的请求。通常是加在网关服务中,可以对发向网关的请求进行全局身份认证、全局限流、日志记录(记录所有的请求信息)、统一修改请求的Header等。
2.5、拦截器
拦截器(Interceptor)是Spring MVC提供的组件,和过滤器一样,也是用于拦截请求并执行预处理或者后处理逻辑。继承HandlerInterceptorAdapter类,preHandle是预处理方法(在请求前执行),postHandle是后处理方法(在请求后执行)。拦截器通常会用在对接口的权限控制。使用preHandle进行请求预处理,没有权限则拦截。也可以记录请求情况日志,使用postHandle在请求后记录日志。
3、代码实现
【免费】基于OAuth2+SpringSecurity+Jwt实现身份认证和权限管理后端服务代码合集资源-CSDN文库
3.1、Eureka注册中心
所有的服务都要向注册中心注册,以便于服务发现和服务之间的调用。
pom.xml
4.0.0 org.example eureka-center 1.0-SNAPSHOT UTF-8 8 8 UTF-8 1.5.4.RELEASE 1.3.5.RELEASE org.springframework.cloud spring-cloud-starter-eureka-server ${springframework.version1} org.springframework.boot spring-boot-starter ${springframework.version} org.springframework.boot spring-boot-starter-test ${springframework.version} test
application.yml
server: port: 8001 #Eureka配置 eureka: instance: hostname: localhost #Eureka服务端的实例名称 client: register-with-eureka: false #是否向eureka注册中心注册自己,因为这里本身就是eureka服务端,所以无需向eureka注册自己 fetch-registry: false #fetch-registry为false,则表示自己为注册中心 service-url: #监控页面 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
SpringcloudEurekaApplication.java
package eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** * @author: Wulc * @createTime: 2025-05-02 * @description: * @version: 1.0 */ @SpringBootApplication @EnableEurekaServer //使eureka服务端可以工作 public class SpringcloudEurekaApplication { public static void main(String[] args) { SpringApplication.run(SpringcloudEurekaApplication.class, args); } }
3.2、auth-service认证授权中心
认证授权中心是用于对用户进行身份认证,授权可以访问的范围,生成token,管理token。
auth-service这部分的代码我是直接用这篇文章里的:OAuth2.0 实现单点登录_oauth2.0单点登录-CSDN博客
因为密码要加密存储,我这里用的是证书加密。
-- 创建数据库证书用于对密码进行加密 --查看数据库中的证书 select * from sys.certificates; --创建数据库主密钥 CREATE MASTER KEY ENCRYPTION BY PASSWORD ='123@#456'; --创建证书 CREATE CERTIFICATE MyCert with SUBJECT = 'Certificate To Password' GO -- 用户表 CREATE TABLE UserInfo ( id int primary key identity(1,1), userName varchar(50), pwd varbinary(2000) ); --使用MyCert证书加密pwd字段 insert into UserInfo(userName,pwd) values('zhangsan', ENCRYPTBYCERT( CERT_ID('MyCert') ,'123456' ) ); insert into UserInfo(userName,pwd) values('lisi', ENCRYPTBYCERT( CERT_ID('MyCert') ,'qwerty' ) ); insert into UserInfo(userName,pwd) values('wangwu', ENCRYPTBYCERT( CERT_ID('MyCert') ,'112233' ) ); --使用MyCert证书解密pwd字段 select id,userName,CONVERT( varchar(100), DecryptByCert ( CERT_ID('MyCert'),pwd ) ) as pwd from UserInfo;
pom.xml
4.0.0 org.example auth-service 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.7.6 UTF-8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.cloud spring-cloud-starter-oauth2 2.2.5.RELEASE org.springframework.cloud spring-cloud-starter-netflix-eureka-server 3.1.1 com.microsoft.sqlserver mssql-jdbc 9.4.0.jre8 com.baomidou mybatis-plus-boot-starter 3.4.0 org.springframework.boot spring-boot-starter-test 2.7.11 junit junit 4.13.1 test org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.projectlombok lombok true org.springframework.cloud spring-cloud-dependencies 2021.0.2 pom import
application.yml
server: port: 8002 servlet: #为了防止一会在服务之间跳转导致Cookie打架(因为所有服务地址都是localhost,都会存JSESSIONID) #这里修改一下context-path,这样保存的Cookie会使用指定的路径,就不会和其他服务打架了 #但是注意之后的请求都得在最前面加上这个路径 context-path: /sso spring: application: name: auth-service-server datasource: name: MyTestDataBase driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://127.0.0.1:1433;databaseName=MyTestDataBase username: wlc password: 123456 redis: port: 6379 database: 0 host: 127.0.0.1 password: mybatis: mapper-locations: classpath:mapper/*.xml #注意:一定要对应mapper映射xml文件的所在路径 eureka: client: service-url: defaultZone: http://localhost:8001/eureka/ # Eureka注册中心地址 register-with-eureka: true fetch-registry: true instance: prefer-ip-address: true instance-id: ${spring.application.name}:${server.port}
OAuth2Configuration.java
package com.auth.config; import com.auth.service.impl.MyUserDetailsService; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 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.token.AuthorizationServerTokenServices; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import javax.annotation.Resource; /** * @author: Wulc * @createTime: 2025-05-12 * @description: * @version: 1.0 */ @EnableAuthorizationServer //开启验证服务器 @Configuration public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter { @Resource private MyUserDetailsService myUserDetailsService; @Resource private AuthenticationManager manager; @Resource private TokenStore store; @Resource private JwtAccessTokenConverter converter; private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints .tokenServices(serverTokenServices()) .userDetailsService(myUserDetailsService) .authenticationManager(manager); } /** * 这个方法是对客户端进行配置,一个验证服务器可以预设很多个客户端, * 之后这些指定的客户端就可以按照下面指定的方式进行验证 * @param clients 客户端配置工具 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients .inMemory() // 这里我们直接硬编码创建,当然也可以像Security那样自定义或是使用JDBC从数据库读取 .withClient("web") // 客户端ID,随便起就行 .secret(encoder.encode("654321")) // 只与客户端分享的secret,随便写,但是注意要加密 .autoApprove(false) // 自动审批,这里关闭,要的就是一会体验那种感觉 .scopes("user") .authorizedGrantTypes("client_credentials", "password", "implicit", "authorization_code", "refresh_token"); } @Override public void configure(AuthorizationServerSecurityConfigurer security) { security .passwordEncoder(encoder) // 编码器设定为BCryptPasswordEncoder .allowFormAuthenticationForClients() // 允许客户端使用表单验证,一会我们POST请求中会携带表单信息 .checkTokenAccess("permitAll()"); // 允许所有的Token查询请求 } /**************************** JWT 配置 **********************************/ private AuthorizationServerTokenServices serverTokenServices(){ // 这里对AuthorizationServerTokenServices进行一下配置 DefaultTokenServices services = new DefaultTokenServices(); services.setSupportRefreshToken(true); // 允许Token刷新 services.setTokenStore(store); // 添加刚刚的TokenStore services.setTokenEnhancer(converter); // 添加Token增强,其实就是JwtAccessTokenConverter,增强是添加一些自定义的数据到JWT中 services.setAccessTokenValiditySeconds(60); //访问token有效期20秒 services.setRefreshTokenValiditySeconds(120); //刷新token有效期120秒 services.setSupportRefreshToken(true); return services; } }
SecurityConfiguration.java
package com.auth.config; import com.auth.service.impl.MyUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; /** * @author: Wulc * @createTime: 2025-05-12 * @description: * @version: 1.0 */ @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailsService myUserDetailsService; @Autowired private RedisConnectionFactory redisConnectionFactory; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); //从数据库中获取用户信息 auth.userDetailsService(myUserDetailsService).passwordEncoder(encoder); } @Bean // 这里需要将AuthenticationManager注册为Bean,在OAuth配置中使用 @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean @Override public UserDetailsService userDetailsServiceBean() throws Exception { return super.userDetailsServiceBean(); } /***************************** JWT配置 ************************************/ @Bean("tokenConverter") public JwtAccessTokenConverter tokenConverter(){ // Token转换器,将其转换为JWT JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("wlcKey"); // 这个是对称密钥,一会资源服务器那边也要指定为这个 return converter; } //token存放在哪里,放在Redis里面 @Bean public TokenStore tokenStore(){ return new RedisTokenStore(redisConnectionFactory); } }
UserInfoDTO.java
package com.auth.dto; import lombok.Data; /** * @author: Wulc * @createTime: 2025-05-12 * @description: * @version: 1.0 */ @Data public class UserInfoDTO { private Integer id; private String userName; private String pwd; }
UserMapper.java
package com.auth.mapper; import com.auth.dto.UserInfoDTO; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper { UserInfoDTO getUserInfoByUserName(String userName); }
MyUserDetailsService.java
package com.auth.service.impl; import com.auth.dto.UserInfoDTO; import com.auth.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; /** * @author: Wulc * @createTime: 2025-05-12 * @description: * @version: 1.0 */ @Service public class MyUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; /** * loadUserByUsername * * description 从数据库中根据用户名获取用户信息,并转为Spring Security的User * @param username * @return * @throws * @author Wulc * @date 2025/5/12 11:01 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserInfoDTO userInfoDTO=userMapper.getUserInfoByUserName(username); List authorities = AuthorityUtils.createAuthorityList("user"); return new User(userInfoDTO.getUserName(), new BCryptPasswordEncoder().encode(userInfoDTO.getPwd()), authorities); } }
ApplicationStarter.java
package com.auth; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author: Wulc * @createTime: 2025-05-12 * @description: * @version: 1.0 */ @SpringBootApplication @EnableDiscoveryClient public class ApplicationStarter { public static void main(String[] args) { SpringApplication.run(ApplicationStarter.class, args); } }
UserMapper.xml
SELECT id, userName, CONVERT( varchar(100), DecryptByCert ( CERT_ID('MyCert'),pwd ) ) as pwd FROM UserInfo WHERE userName = #{userName}
启动该服务后:
访问:http://localhost:8002/sso/oauth/token 获取到token。
因为token是存放在redis里面的,可以在redis里面查看到token。
访问:http://localhost:8002/sso/oauth/check_token 可以检查token是否有效。
访问:http://localhost:8002/sso/oauth/token 可以在access_token过期时,使用refresh_token重新获取一遍token。这样子就避免了用户再次输入用户名密码了。
3.3、action-controller-service权限控制中心
AccessActionControl.java
package com.action.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author Wulc * @date 2025/5/13 8:55 * @description 定义注解用于加在接口方法上进行权限控制 */ @Target({ElementType.METHOD})// 可用在方法名上 @Retention(RetentionPolicy.RUNTIME)// 运行时有效 @Documented public @interface AccessActionControl { String[] resource() default {}; String[] action() default {}; }
AccessActionFeign.java
package com.action.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(value = "action-controller-server") public interface AccessActionFeign { @PostMapping("/api/checkAccessAction") boolean checkAccessAction(@RequestParam("username") String username, @RequestParam("resource") String[] resource, @RequestParam("action") String[] action); }
pom.xml(action-controller-api)
4.0.0 org.example action-controller-service 1.0-SNAPSHOT action-controller-api jar UTF-8 org.springframework.cloud spring-cloud-starter-openfeign org.springframework.boot spring-boot-maven-plugin true org.apache.maven.plugins maven-source-plugin attach-sources jar-no-fork
action-controller-server
application.yml
server: port: 8004 spring: application: name: action-controller-server #eureka配置,服务注册到哪? eureka: client: service-url: defaultZone: http://localhost:8001/eureka/ instance: #修改eureka上默认描述信息 instance-id: ${spring.application.name}:${server.port}
AccessActionController.java
package com.action.controller.feign; import com.action.feign.AccessActionFeign; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author: Wulc * @createTime: 2025-05-13 * @description: * @version: 1.0 */ @RestController @RequestMapping("/api") public class AccessActionController implements AccessActionFeign { @PostMapping("/checkAccessAction") @Override public boolean checkAccessAction(@RequestParam("username") String username, @RequestParam("resource") String[] resource, @RequestParam("action") String[] action) { //这里可以写你的权限判断逻辑,通常是根据数据库中的角色表权限表计算出来的。 //我这里作为例子,就直接写死了 if ("zhangsan".equals(username) && "1086".equals(resource[0]) && "read".equals(action[0])) { return true; } return false; } }
ApplicationStarter.java
package com.action; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author: Wulc * @createTime: 2025-05-13 * @description: * @version: 1.0 */ @EnableDiscoveryClient @SpringBootApplication public class ApplicationStarter { public static void main(String[] args) { SpringApplication.run(ApplicationStarter.class, args); } }
pom.xml(action-controller-server)
4.0.0 org.example action-controller-service 1.0-SNAPSHOT action-controller-server UTF-8 org.example action-controller-api 1.0-SNAPSHOT
pom.xml(action-controller-service)
4.0.0 org.example action-controller-service 1.0-SNAPSHOT pom action-controller-api action-controller-server org.springframework.boot spring-boot-starter-parent 2.7.3 8 8 UTF-8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.boot spring-boot-starter-aop org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-dependencies 2021.0.3 pom import org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-jar-plugin com.action.ApplicationStarter wulc-nexus http://192.168.10.104:8081/repository/maven-snapshots/
关于如果上传到nexus可以参考我的这篇:使用Nexus搭建远程maven仓库_nexus 仓库教程-CSDN博客
当然如果嫌搭建一个Nexus太麻烦的话,可以直接本地对action-controller-api进行maven install,在本地maven仓库中生成一个jar包。供其他服务需要时直接导入。
3.4、provider-server
provider-server是被访问的服务,会引入action-controller-api依赖,在服务的接口上加上@AccessActionControl用于方法级别的权限控制。会写一个拦截器,用于对所有加了@AccessActionControl注解的接口进行权限判断预处理。
pom.xml
4.0.0 org.example provider-server 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.7.3 8 8 UTF-8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.example action-controller-api 1.0-SNAPSHOT org.springframework.cloud spring-cloud-dependencies 2021.0.3 pom import org.springframework.boot spring-boot-maven-plugin wulc-nexus wulc-nexus http://192.168.10.104:8081/repository/maven-public/ true true
注:pom.xml中的的配置表示直接从192.168.10.104:8081上的nexus仓库中获取action-controller-api的jar包。当然你也可以直接引入action-controller-api的jar包。
package com.provider.config; import com.provider.interceptor.AccessActionInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author: Wulc * @createTime: 2025-05-13 * @description: * @version: 1.0 */ @Configuration public class WebConfiguration implements WebMvcConfigurer { @Autowired @Lazy // 延迟注入,避免循环依赖 AccessActionInterceptor accessActionInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //拦截所有请求 registry.addInterceptor(accessActionInterceptor).addPathPatterns("/**"); } }
ProviderController.java
package com.provider.controller; import com.action.annotation.AccessActionControl; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author: Wulc * @createTime: 2025-05-12 * @description: * @version: 1.0 */ @RestController @RequestMapping("/provider") public class ProviderController { @AccessActionControl(resource = {"1086"}, action = "read") @PostMapping("/getMsg") public String getMsg(){ return "访问到了provider"; } }
AccessActionInterceptor.java
package com.provider.interceptor; import com.action.annotation.AccessActionControl; import com.action.feign.AccessActionFeign; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; /** * @author: Wulc * @createTime: 2025-05-13 * @description: * @version: 1.0 */ @Component public class AccessActionInterceptor extends HandlerInterceptorAdapter { @Autowired private AccessActionFeign accessActionFeign; //在请求被处理之前,调用action-controller的权限判断接口,如果有权限就放行,没有权限就拦截 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果不是映射到方法直接通过 if (!(handler instanceof HandlerMethod)) { return true; } // ①:START 方法注解级拦截器 HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); AccessActionControl accessActionControl=method.getAnnotation(AccessActionControl.class); if(accessActionControl!=null){ String username=request.getHeader("username"); String[] resource=accessActionControl.resource(); String[] action=accessActionControl.action(); // accessActionFeign.checkAccessAction(username,resource,action); boolean flag=accessActionFeign.checkAccessAction(username,resource,action); if(!flag){ // 设置响应状态码(如403 Forbidden) response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 设置响应内容类型(如JSON) response.setContentType("application/json;charset=UTF-8"); // 构建响应内容(示例:返回JSON格式错误信息) String errorMessage = "{\"code\":403,\"message\":\"权限不足,禁止访问\"}"; // 写入响应体 response.getWriter().write(errorMessage); // 关闭输出流(重要!) response.getWriter().close(); return flag; } } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { super.postHandle(request, response, handler, modelAndView); } }
ApplicationStarter.java
package com.provider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; /** * @author: Wulc * @createTime: 2025-05-01 * @description: * @version: 1.0 */ @EnableDiscoveryClient @EnableFeignClients({"com.action.feign"}) @SpringBootApplication public class ApplicationStarter { public static void main(String[] args) { SpringApplication.run(ApplicationStarter.class, args); } }
application.yml
server: port: 8003 spring: application: name: provider-server #eureka配置,服务注册到哪? eureka: client: service-url: defaultZone: http://localhost:8001/eureka/ instance: #修改eureka上默认描述信息 instance-id: ${spring.application.name}:${server.port}
3.5、SpringCloud网关
网关的作用是进行反向代理,把客户端的请求转发到对应的服务端。这里的网关是集成了身份认证服务。客户端的请求发送到网关,会先经过网关的全局过滤器,在过滤器中先去判断客户端的请求中是否有携带token?如果携带了token,则去redis中验证该token是否有效?如果token有效则过滤器放行,如果token失效了则使用refresh_token去调用http://localhost:8002/sso/oauth/token接口重新获取token,获取到新token后,过滤器再放行。
pom.xml
4.0.0 org.example cloud-gateway 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.7.3 8 8 UTF-8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.springframework.cloud spring-cloud-starter-gateway org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-loadbalancer org.springframework.cloud spring-cloud-dependencies 2021.0.3 pom import org.springframework.boot spring-boot-maven-plugin
application.yml
server: port: 8000 spring: main: web-application-type: reactive # 强制使用WebFlux application: name: cloud-gateway-service profiles: include: route #使用application-route.yml里面的配置 eureka: client: service-url: defaultZone: http://localhost:8001/eureka/ # Eureka注册中心地址 register-with-eureka: true fetch-registry: true instance: prefer-ip-address: true instance-id: ${spring.application.name}:${server.port}
application-route.yml
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowed-origin-patterns: '*' #允许所有的跨域 allowed-headers: '*' #允许所有的头 allowed-methods: '*' #允许所有的请求方式 discovery: locator: enabled: true # 开启从注册中心动态创建路由 lower-case-service-id: true # 服务名小写 routes: - id: route1 uri: lb://provider-server # lb表示负载均衡 loadbalance predicates: #断定,遵守哪些规则,就把请求转发给wulc-test-consumer-server这个服务 - Path=/api/provider/** filters: - StripPrefix=1
pom.xml
4.0.0 org.example cloud-gateway 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.7.3 8 8 UTF-8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.springframework.cloud spring-cloud-starter-gateway org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-loadbalancer org.springframework.cloud spring-cloud-dependencies 2021.0.3 pom import org.springframework.boot spring-boot-maven-plugin
RedisConfig.java
package com.gateway.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @author Wulc * @date 2024/4/8 11:39 * @description */ @Configuration public class RedisConfig { @Bean public RedisTemplate redisTemplate1(RedisTemplate redisTemplate) { RedisSerializer stringSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringSerializer); redisTemplate.setStringSerializer(stringSerializer); redisTemplate.setValueSerializer(stringSerializer); redisTemplate.setHashKeySerializer(stringSerializer); redisTemplate.setHashValueSerializer(stringSerializer); return redisTemplate; } }
GatewayGlobalFilter.java
package com.gateway.filter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.util.Base64Utils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.annotation.Resource; import java.nio.charset.StandardCharsets; import java.util.Map; /** * @author: Wulc * @createTime: 2025-05-12 * @description: 过滤器,当请求发送到网关时,先走过滤器进行身份认证,再路由转发 * @version: 1.0 */ @Component public class GatewayGlobalFilter implements GlobalFilter { @Resource private RedisTemplate redisTemplate; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { //获取请求头 HttpHeaders headers = exchange.getRequest().getHeaders(); String authorization = headers.getFirst("Authorization"); String accessToken = authorization.substring(7); String refreshAccessToken = headers.get("Refresh").get(0); String accessKey = "access" + ":" + accessToken; String refreshAccessKey = "refresh" + ":" + refreshAccessToken; RestTemplate restTemplate = new RestTemplate(); //判断access_token在redis中是否存在 if (redisTemplate.opsForValue().get(accessKey) == null) { //如果access_token在redis中不存在,但refresh_access_token存在,则用refresh_access_token自动重新认证一下 if (redisTemplate.opsForValue().get(refreshAccessKey) != null) { //构建表头数据 HttpHeaders requestHeaders = new HttpHeaders(); String auth = "web" + ":" + "654321"; String encodedAuth = Base64Utils.encodeToString(auth.getBytes()); requestHeaders.set("Authorization", "Basic " + encodedAuth); // 构建表单数据 MultiValueMap formData = new LinkedMultiValueMap(); formData.add("refresh_token", refreshAccessToken); formData.add("grant_type", "refresh_token"); // 构建请求实体 HttpEntity requestEntity = new HttpEntity(formData, requestHeaders); try { ResponseEntity responseEntity = restTemplate.exchange("http://localhost:8002/sso/oauth/token", HttpMethod.POST, requestEntity, Map.class); return chain.filter(exchange); } catch (Exception ex) { return sendErrorResponse(exchange, HttpStatus.UNAUTHORIZED, "请登录"); } } return sendErrorResponse(exchange, HttpStatus.UNAUTHORIZED, "请登录"); } //继续后续处理 return chain.filter(exchange); } /** * sendErrorResponse ** description //返回错误信息 * * @param * @return * @throws * @author Wulc * @date 2025/5/12 22:30 */ private Mono sendErrorResponse(ServerWebExchange exchange, HttpStatus status, String message) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(status); response.getHeaders().setContentType(MediaType.TEXT_PLAIN); byte[] bytes = message.getBytes(StandardCharsets.UTF_8); DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); return response.writeWith(Flux.just(buffer)); } }
ApplicationStarter.java
package com.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author: Wulc * @createTime: 2025-05-12 * @description: * @version: 1.0 */ //gateway服务一定要等其他服务启动注册eureka成功后,再最后启动 @SpringBootApplication @EnableDiscoveryClient public class ApplicationStarter { public static void main(String[] args) { SpringApplication.run(ApplicationStarter.class, args); } }
启动网关服务:
先调用接口:http://localhost:8002/sso/oauth/token进行身份认证,并获取token信息。
使用从/oauth/token接口获取的token访问网关:http://localhost:8000/api/provider/getMsg 先经过网关的过滤器,根据token判断用户是否认证?如果是认证用户,网关会根据yml里面配置的路由将/api/provider/getMsg请求转发到相应的后端服务上。
如果username="wangwu",因为wangwu没有在action-controller-server中checkAccessAction方法中配置权限,所以wangwu用户是没有访问权限的。会被provider-server的拦截器给拦截掉。
只有当username="zhangsan"时,才有访问权限。
以上就是基于OAuth2+SpringSecurity+Jwt+过滤器+拦截器+注解+feign+网关+Eureka实现的一个简易身份认证和权限管理系统。
4、总结
实现一个鉴权系统其实只要有token+拦截器就行了,身份认证用token,权限控制用拦截器。使用OAuth2框架是为了应对不同的场景,比如隐式授权模式用来实现单点登录,授权码模式用于实现第三方登录,即通过验证服务器去代理客户端进行身份验证,而不是让客户端拿着token去身份认证。
而Spring Security本身提供了一个登录页面,但实际中不会用到。Spring Security提供了完整的鉴权、授权、会话管理、防护攻击(如CRSF跨站请求伪造、XSS跨站脚本攻击)。Spring Security默认开启CRSF Token验证防护,对所有的请求(Post、Put、Delete)统统要求携带有效token。防护XSS攻击会设置一些内容安全策略,限制外部访问,白名单黑名单等。
其实对于鉴权系统而言,最难反而是根据业务设计一个权限控制模型。常用的权限模型有RBAC和ABAC两种。RBAC是基于角色的权限模型,角色是权限的集合,通过定义权限组(角色),把权限组授权给用户。而ABAC是基于属性的访问控制,是在RBAC的基础上更进一步细粒度的控制权限。比如某个用户有访问文档库的权限,这个可以用角色去授权文档资源。每个用户只能访问自己所属团队的文档,这个要基于团队属性进行授权。
在实际的授权中,通常会用到这些表:
- 用户表:存储用户基本信息,用户名&密码等,敏感信息要加密处理。
- 角色表:存储角色的定义,角色Id,角色名,角色说明等字段。
- 权限表:存储系统中各种可被访问的资源,权限Id,资源Id,资源名称,资源操作,说明等字段。
- 角色权限表:角色Id,权限Id。
- 用户角色表:用户Id,角色Id。
以上这些是基于角色的访问控制RBAC,是外部权限。
基于属性的访问控制ABAC,通常会写在权限判断的方法里,定制化更强一些,是内部权限。
外部权限+内部权限,RBAC+ABAC共同构成了权限控制。
至于实际过程中如何使用?这里举一个简单的例子:
@AccessActionControl(resource = {"1086"}, action = "read")这个注解会加在接口方法上,用于对接口方法进行权限控制。会传入“资源resource”和“动作action”。根据“资源”和“动作”去“权限表”中获取对应的权限Id,然后根据权限Id在“角色权限表”获取哪些角色有该权限(记为arryRoles1)。根据用户Id在“用户角色表”在查询该用户有哪些角色(记为arrRoles2)。最后只要判断arrRoles2和arryRoles1有没有交集就行了。如果有交集就说明该用户有权限,如果没有就说明该用户没有权限。
5、参考资料
2、用户认证和授权哔哩哔哩bilibili
springsecurity+jwt+oauth2.0入门到精通视频教程【免费学习】哔哩哔哩bilibili
Spring Cloud 微服务安全:OAuth2 + JWT 实现认证与授权_springcloud oauth2 jwt-CSDN博客
OAuth2.0 实现单点登录_oauth2.0单点登录-CSDN博客
Sql Server数据库实现表中字段的列加密研究_sql实现对密码字段加密-CSDN博客
Spring Security实现从数据库中访问用户名和密码实现登录_spring security5 实现数据库登录-CSDN博客
OAuth2.0系列之信息Redis存储实践(七) - smileNicky - 博客园
IDEA使用系列之导入外部jar包_idea添加外部jar包-CSDN博客