1. 简介
在微服务架构中,系统被拆分成许多小型、独立的服务,每个服务负责一个功能模块。这种架构风格带来了一系列的优势,如服务的独立性、弹性、可伸缩性等。然而,它也带来了一些挑战,特别是在安全性方面。这时候就体现出认证服务器的重要性,它可以在网关服务器的基础上做登录认证,权限认证等功能。本篇文章就以如下结构实现一个demo供大家参考选择,整体逻辑如下图所示
- 当客户端第一次发起资源请求(一般前端会处理好逻辑,比如vue中实现未登录的用户无法访问系统资源等)
- gateway拦截到请求并检查请求头中是否携带token,有则放行没有则无权限无法访问(返回401)
- 客户端接拦截到响应并解析出当前响应状态码是401,则会redirect到登录页面(未登录的用户请先登录)gateway拦截到请求后判断当前是登录url则放行,转发到认证服务器进行登录操作
- 根据email / username判断是否存在数据库,存在则取出数据对登录密码进行加密比对,比对通过则代表成功登录生成token,并且获取该用户所对应角色的权限信息,并将其存在redis中
- 用户端拦截到登录响应数据,从其中获取到token和一些用户信息保存到本地(session,localStorage等)
- 登录后的每次请求发送前都会在请求头中添加token信息(本次实现鉴权逻辑不写在认证服务器中,由每个资源服务器引入jar包依赖各自鉴权)
- 通过
@PreAuthorize
注解进行判断当前用户是否有执行该方法相应的权限,如果有则顺利执行方法返回结果,否则无权限返回code401
2. 认证服务器实现
由于本次实现中,认证服务器负责的功能就是登录(查询用户信息,登出) 、 查找权限、对token的签发、刷新管理,所以该服务器就不考虑集成springsecurity,只需引入mybatis相关依赖和nacos服务注册发现的
2.1 微服务配置
2.1.1 改pom
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 支持负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--cloud_commons_utils-->
<dependency>
<groupId>com.simple.cloud</groupId>
<artifactId>simpleCloud_api_commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided </scope>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2.1.2 application.yml 配置
server:
port: 10001
spring:
application:
name: auth-server
cloud:
nacos:
discovery:
server-addr: localhost:8848
data:
redis:
host: localhost
port: 6379
database: 0
timeout: 1800000
password:
jedis:
pool:
max-active: 20 #最大连接数
max-wait: -1 #最大阻塞等待时间(负数表示没限制)
max-idle: 5 #最大空闲
min-idle: 0 #最小空闲
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver # 用户登录表所在的数据库
url: jdbc:mysql://localhost:3306/seata_system?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: abc123
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.simple.cloud.entities
configuration:
map-underscore-to-camel-case: true
2.1.3 主启动
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.simple.cloud.mapper")
public class AuthMain10001 {
public static void main(String[] args) {
SpringApplication.run(AuthMain10001.class , args);
}
}
2.2 需求功能实现
跟springsecurity的逻辑一样,我们需要提供一个加密器和一个UserDetailService并定义findByUsername
方法,话不多说下面就跟我一起一一实现吧
2.2.1 utils工具类
2.2.1.1 SHA-256 加密器
在选择加密或哈希算法时,更推荐使用SHA-256或SHA-3这两个方法都属于SHA(安全散列算法)系列,它们提供了比MD5更强的安全性。SHA-256生成的是256位的哈希值,而SHA-3是最新的成员,提供了与SHA-2类似的安全性,但采用了不同的算法设计。这些算法在生成数字签名和验证数据完整性方面被广泛使用。本篇教程基于SHA-256实现密码加密
当然如果作者想基于对称加密是西安,AES(高级加密标准)是目前推荐的算法。感兴趣的读者可以去了解了解😁
public class SHA_256Helper {
public static String encrypt(String password) {
try {
// 获取SHA-256 MessageDigest实例
MessageDigest digest = MessageDigest.getInstance("SHA-256");
// 将输入字符串转换为字节数组
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
// 将字节数组转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 encoded fail!!+" + e);
}
}
}
2.2.1.2 JWTUtiles
JWT(JSON Web Token)是一种开放标准,它允许在两方之间安全地传输信息。由于JWT是经过数字签名的,因此它的内容不仅可以被校验,而且可以被信任。这使得JWT成为在用户登录场景中存储用户登录状态、过期时间等信息的理想选择。
将权限信息存储在JWT中的做法通常包括以下步骤:
- 编码权限信息:在生成JWT时,可以将用户的权限信息作为有效载荷的一部分进行编码。这些信息可以是角色、权限级别或其他与用户相关的访问控制数据。
- 传输token:当用户登录成功并获得了JWT后,前端会在后续的请求中携带这个JWT。这样,后端就可以通过解析JWT来验证用户的权限信息。
- 解析和验证:后端接收到含有JWT的请求时,会首先对JWT进行解码和验证。验证成功后,就可以从JWT的有效载荷中读取出用户的权限信息,并根据这些信息来判断用户是否有权访问请求的资源或执行操作。
需要注意的是,尽管可以将这些信息放入JWT,但也要考虑安全性问题。例如,不应将敏感信息放入JWT的有效载荷中,因为有效载荷是可以被解码的。此外,应该设置合理的过期时间,并在必要时提供刷新机制,以便在不重新进行完整身份验证的情况下更新令牌。
public class JWTHelper {
private static long tokenExpiration = 20 * 60 * 1000; // 20min过期
private static long tokenRefreshExpiration = 12 * 60 * 60 * 1000; // 12小时过期
private static String tokenSignKey = "31c78b41f"; //密钥
private static String buildToken(Long userId, String email, List<String> permission , long timeToLive){
return Jwts.builder()
.setSubject("AUTH-USER")
.setExpiration(new Date(System.currentTimeMillis() + timeToLive))
.claim("userId", userId)
.claim("email", email)
.claim("permission", permission)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
}
public static String[] createToken(Long userId, String email, List<String> permission) {
String token = buildToken(userId,email,permission,tokenExpiration);
//token过期时可以刷新长期token
String refreshToken = buildToken(userId,email,permission,tokenRefreshExpiration);
return new String[]{token , refreshToken};
}
// 原始token过期时刷新token 而refreshToken保持不变(如果refresh都过期则需重新登录)
public static String refresh(String refreshToken){
return buildToken(SecurityAccessConstant.TOKEN_TYPE, getUserId(refreshToken) ,getEmail(refreshToken)
, getPermission(refreshToken) , tokenExpiration);
}
// 去掉前缀
public static String getToken(String token){
if(token == null)
return null;
if(token.startsWith(SecurityAccessConstant.TOKEN_PREFIX))
return token.replace(SecurityAccessConstant.TOKEN_PREFIX,"");
//没带前缀的认为是无效token
return null;
}
// 获取当前token过期时间
public static Date getExpirationDate(String token) {
if(StringUtil.isBlank(token))
return null;
Claims claims = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody();
return claims.getExpiration();
}
//判断当前token是否过期
public static boolean isOutDate(String token){
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Date expirationDate = claimsJws.getBody().getExpiration();
return expirationDate.before(new Date());
} catch (JwtException e) {
// JWT token无效或已损坏
return true;
}
}
public static Long getUserId(String token) {
try {
if (token == null || token == "") return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer) claims.get("userId");
return userId.longValue();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static String getEmail(String token) {
try {
if (token == null || token == "") return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String) claims.get("email");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static List<String> getPermission(String token) {
try {
if (token == null || token == "") return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (List<String>) claims.get("permission");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
对于对token无感刷新感兴趣的读者可以查阅 对token无感刷新的理解,里面提到了具体的实现逻辑和编码过程中的一些思考😁希望能对你有所帮助
2.2.1.3 SecurityAccessConstant 定义一些全局常量
往往是一些字符串类型的关键字,在这里统一定义外部就可以直接调用,方便微服务之间的管理
public class SecurityAccessConstant {
public static String TOKEN_PREFIX = "Bearer ";
public static String HEADER_NAME_TOKEN = "Authorization";
public static String TOKEN_TYPE = "Short-lived";
public static String REFRESH_TOKEN_TYPE = "refresh";
public static String WEB_REQUEST_TO_AUTH_URL = "http://127.0.0.1:10001";
public static String REQUEST_LOGGING_URI = "/simple/cloud/access/login";
public static String REQUEST_REFRESH = "/simple/cloud/access/refresh";
public static String USERINFO_REDIS_STORAGE_KEY = "_INFO_dbh9";
public static String REFRESH_TOKEN_REDIS_STORAGE_KEY = "_REFRESH_s9k1";
}
2.2.1.4 ResponseUtil
在后面就可以看到,springboot3响应式编程里的filter使用的是ServerWebExchange,所以这里就会对该ServerWebExchange实例修改其响应返回而不继续执行后面的逻辑 其中响应修改的内容有响应状态码StatusCode
和可能携带的响应信息RespondBody
具体封装响应体的写法可以看return处(使用writeWith封装)
public class ResponseUtils {
public static Mono<Void> out(ServerWebExchange exchange, ResultData r){
// 将ResultData对象转换为JSON字符串,并设置为响应体
ObjectMapper objectMapper = new ObjectMapper();
byte[] responseBody = new byte[0];
try {
responseBody = objectMapper.writeValueAsBytes(r);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
exchange.getResponse().setStatusCode(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED);
exchange.getResponse().getHeaders().add("Content-Type", "application/json");
return exchange.getResponse().writeWith(Mono.just(new DefaultDataBufferFactory().wrap(responseBody)));
}
/**
* 使用WebClient异步访问 localhost:10001/auth/login 为例子
* @param url http://localhost:10001 前缀
* @param uri /auth/login 后边的路径名称
* @param key,value 请求头中的键值对
* @return
*/
public static Mono<ResultData> webClientRequest(String url , String uri , String key , String value){
WebClient webClient = WebClient.create(url);
Mono<ResultData> response = webClient.get()
.uri(uri)
.header(key , value)
.retrieve()
.bodyToMono(ResultData.class);
return response;
}
}
2.3 主体功能实现
该controller即是本次认证服务器实现的所以方法:
- login: 登录方法,接收一个LoginVo 类型的登录数据(其中包含了email和password),首先根据邮箱去数据库找是否有该用户,如果有则继续对密码加密然后比对,当比对成功时则会查询该用户所有的权限信息本次实现的权限是通过与meau表集成,即根据type判断是权限还是菜单如下图(这部分根据自己的需求来自定义,拆开也可以)之后将所有需要返回的数据放入map中统一返回就好了,这里包括了
token
、tokenExpire
(便于前端判断token是否过期动态刷新)和refreshToken
(用于短token刷新的凭证) - refresh : 即是上面使用refreshToken 来刷新token的实现方法,注意这里返回的结果为新下发的token和其过期时间
- info : 该方法用户获取用户信息(这里就偷懒了从redis取出直接返回,想做更细化功能的读者可以在此基础上扩展)
- logout : 退出登录接口,用于提醒服务器删掉保存的一些信息(比如redis或者消息队列中的)防止信息泄露
@RestController
@RequestMapping("/simple/cloud/access")
public class AuthController {
@Resource
private SysUserService sysUserService;
@Resource
private SysMenuService sysMenuService;
@Resource
private RedisTemplate redisTemplate;
/**
* 登录
* @return
*/
@PostMapping("/login")
public ResultData login(@RequestBody LoginVo loginVo) throws Exception{
// 先根据email找指定用户
SysUser sysUser = sysUserService.findUserByEmail(loginVo.getEmail());
if(sysUser == null)
throw new Exception("找不到该用户");
//加密密码来比较
String encryptValue = SHA_256Helper.encrypt(loginVo.getPassword());
if(!StringUtils.pathEquals(encryptValue,sysUser.getPassword()))
throw new Exception("密码错误");
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"+encryptValue);
//获取用户的角色
List<SysRole> sysRoles = sysUserService.selectAllByUserId(sysUser.getId());
sysUser.setRoleList(sysRoles);
//根据id获取所有菜单列表
List<RouterVo> routerList = sysMenuService.getAllRouterListByUserId(sysUser.getId());
//根据id获取所有按钮列表
List<String> permsList = sysMenuService.getAllMenuListByUserId(sysUser.getId());
//map中插入相应的值
Map<String, Object> map = new HashMap<>();
map.put("routers",routerList);
map.put("buttons",permsList);
map.put("roles",sysUser.getRoleList());
map.put("name",sysUser.getName());
//存放token到请求头中
String[] tokenArray = JWTHelper.createToken(sysUser.getId(), sysUser.getEmail(), permsList);
map.put("token",tokenArray[0]);
map.put("tokenExpire",JWTHelper.getExpirationDate(tokenArray[0]).getTime());
map.put("refreshToken",tokenArray[1]);
// 存放用户信息权限数据
redisTemplate.opsForValue().set(sysUser.getId() + SecurityAccessConstant.USERINFO_REDIS_STORAGE_KEY
, new ObjectMapper().writeValueAsString(map)
, 30*60, TimeUnit.SECONDS);
// 存放refreshToken
redisTemplate.opsForValue().set(tokenArray[0]
, tokenArray[1]
, JWTHelper.getExpirationDate(tokenArray[1]).getTime() , TimeUnit.MILLISECONDS);
return ResultData.success(map);
}
@GetMapping("/refresh")
public ResultData refresh(HttpServletRequest request){
String refreshToken = JWTHelper.getToken(request.getHeader(SecurityAccessConstant.HEADER_NAME_TOKEN));
//刷新token
String refresh = JWTHelper.refresh(refreshToken);
Map<String, Object> map = new HashMap<>();
map.put("token",refresh);
map.put("expire",JWTHelper.getExpirationDate(refresh).getTime());
return ResultData.success(map);
}
/**
* 获取用户信息
* @return
*/
@PostMapping("/info")
public ResultData info(HttpServletRequest request) throws JsonProcessingException {
String token = JWTHelper.getToken(request.getHeader(SecurityAccessConstant.HEADER_NAME_TOKEN));
Long userId = JWTHelper.getUserId(token);
if(userId == null)
return ResultData.fail(ResultCodeEnum.RC401.getCode(), "token失效请重新登录");
// 存放权限信息到redis中 , springsecurity通过 userId 做为key获取权限列表
String storageJSON = (String) redisTemplate.opsForValue()
.get(userId + SecurityAccessConstant.USERINFO_REDIS_STORAGE_KEY);
if(null == storageJSON)
return ResultData.fail(ResultCodeEnum.RC401.getCode(), "登录失败请重新登录");
return ResultData.success(new ObjectMapper().readValue(storageJSON , Map.class));
}
/**
* 退出
* @return
*/
@PostMapping("/logout")
public ResultData logout(HttpServletRequest request){
String token = JWTHelper.getToken(request.getHeader(SecurityAccessConstant.HEADER_NAME_TOKEN));
if(token == null)
return ResultData.success("token失效 以退出登录");
Long userId = JWTHelper.getUserId(token);
if(userId == null)
return ResultData.success("token失效 以退出登录");
redisTemplate.delete(userId + SecurityAccessConstant.USERINFO_REDIS_STORAGE_KEY);
redisTemplate.delete(userId + SecurityAccessConstant.REFRESH_TOKEN_REDIS_STORAGE_KEY);
return ResultData.success("退出成功");
}
}
对于其中service、mapper方法这里就不在细讲,因为不同需求实现的逻辑都不相同,所以提供一个controller给各位读者参考(其实根据方法名可以直到该方法做什么的👍)
到此认证服务器的基本功能就实现完了,总结一下主要做的就是登录认证,token的下发、刷新和维护(对于这块感兴趣的读者可以去查阅另一篇文章 对token无感刷新的理解)
3. 鉴权功能依赖集成
在本次的实战中,我使用在每个微服务引入自定义的鉴权jar包的方法实现鉴权(该鉴权功能通过springsecurity实现)具体架构如下,只需要在需要鉴权的微服务pom.xml中引入该adapter即可
3.1 pom依赖引入
主要就是springsecurity的依赖,另外common是实现的一些工具类jar包,就如2.2.1 所讲述的那些
<dependencies>
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--cloud_commons_utils-->
<dependency>
<groupId>com.simple.cloud</groupId>
<artifactId>simpleCloud_api_commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.2 过滤器实现
核心就是获取到封装权限信息的UsernamePasswordAuthenticationToken ,方法定义如下图所示,其中第二个credentials放的是密码等,但是为了防止泄露在登录成功后会将其设置为null
@Order(1)
public class TokenAuthenticationFilter extends OncePerRequestFilter {
public TokenAuthenticationFilter() {
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("uri:"+request.getRequestURI());
//获取包含权限的authentication
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if(null != authentication) {
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} else {
ResponseUtil.out(response, ResultData.fail(ResultCodeEnum.RC401.getCode(), ResultCodeEnum.RC401.getMessage()));
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//请求头是否有token
String token = JWTHelper.getToken(request.getHeader(SecurityAccessConstant.HEADER_NAME_TOKEN));
if(null != token) {
String email = JWTHelper.getEmail(token);
Long userId = JWTHelper.getUserId(token);
List<String> permission = JWTHelper.getPermission(token);
if(null != permission) {
//当前用户信息放到ThreadLocal里面
LoginUserInfoHelper.setUserId(userId);
LoginUserInfoHelper.setEmail(email);
//把权限数据转换要求集合类型 List<SimpleGrantedAuthority>
List<SimpleGrantedAuthority> collect = permission.stream().map(val -> new SimpleGrantedAuthority(val)).collect(Collectors.toList());
return new UsernamePasswordAuthenticationToken(email, null, collect);
}
}
return null;
}
}
3.3 springsecurity 配置类
注意在springboot3中集成的springsecurity已经淘汰掉继承 WebSecurityConfigurerAdapter 的方法,鼓励开发者自己写配置类将bean注入容器中
由于不需要做登录认证,只需要做权限校验,所以 不需要引入登录相关的filter(userdetailService等那些都不用引入)需要引入的只有前面定义的TokenAuthenticationFilter 和 PasswordEncoder (其实这个也不需要因为并没有在逻辑中用到,但是加上也不妨碍)然后就是根据自己的业务需求配置SecurityFilterChain 就好啦
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
@EnableMethodSecurity //启用方法级别鉴权
public class WebSecurityConfig{
@Bean
public PasswordEncoder passwordEncoder() {
return new CustomSHA_256PasswordEncoder();
}
@Bean
public TokenAuthenticationFilter authenticationJwtTokenFilter() {
return new TokenAuthenticationFilter();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用basic明文验证
.httpBasic().disable()
// 前后端分离架构不需要csrf保护
.csrf().disable()
// 禁用默认登录页
.formLogin().disable()
// 禁用默认登出页
.logout().disable()
// 前后端分离是无状态的,不需要session了,直接禁用。
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
// 允许所有OPTIONS请求
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 允许 SpringMVC 的默认错误地址匿名访问
.requestMatchers("/error").permitAll()
// 允许任意请求被已登录用户访问,不检查Authority
.anyRequest().authenticated())
// 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* 需要调用AuthenticationManager.authenticate执行一次校验
*
* @param config
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
以上就是全部编码,在完成后可以打成jar包供其他微服务引入依赖
3.4 使用鉴权
在需要鉴权的微服务中引入依赖
<!-- security -->
<dependency>
<groupId>com.simple.cloud</groupId>
<artifactId>simpleCloud_security_adapter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
然后在指定的方法上用springsecurity提供的方法进行权限控制就好啦,如下例子
注意注解的使用,在内是 hasAuthority(权限名/类别) ,为了该注解能在方法级别起效必需在springsecurity的配置类上标注注解
@EnableMethodSecurity
启用方法级别鉴权
/**
* 获取所有用户列表
* */
@GetMapping("/listAll")
@PreAuthorize("hasAuthority('bnt.sysUser.list')")
public ResultData getAllUser(){
List<SysUser> sysUsers = sysUserService.findAllUsers();
if(sysUsers != null)
return ResultData.success(sysUsers);
return ResultData.fail(ResultCodeEnum.RC996.getCode(), "查询失败,请联系管理员");
}
4. 总结
基于这种登录认证和权限认证分离的方式设计有好有坏,对于好处而言:
-
集中式认证管理:通过统一的认证服务器进行登录认证和token的签发刷新,可以简化认证流程,提高安全性和效率。
-
灵活性和可扩展性:各个微服务自行处理权限认证,可以根据各自的业务需求灵活设计权限控制逻辑,便于扩展和维护。
-
适应多种鉴权场景:这种方式可以适应外部应用接入、用户-服务鉴权、服务-服务鉴权等多种鉴权场景。
同时也会带来一些坏处:
- 潜在的安全风险:如果各个微服务的权限认证实现不一致或存在缺陷,可能会引入安全风险。
- 性能考虑:每个请求都可能需要经过权限认证,如果没有合理的优化,可能会对系统性能产生影响。
现在基于OAuth2 的权限认证模式也是一种普遍的实现方案,对于上面自研认证服务毕竟还是没有实现高并发场景下的功能,所以有感兴趣的读者可以往这方面继续专研⛽