springboot3微服务下结合springsecurity的认证授权实现

1. 简介

在微服务架构中,系统被拆分成许多小型、独立的服务,每个服务负责一个功能模块。这种架构风格带来了一系列的优势,如服务的独立性、弹性、可伸缩性等。然而,它也带来了一些挑战,特别是在安全性方面。这时候就体现出认证服务器的重要性,它可以在网关服务器的基础上做登录认证,权限认证等功能。本篇文章就以如下结构实现一个demo供大家参考选择,整体逻辑如下图所示

  1. 当客户端第一次发起资源请求(一般前端会处理好逻辑,比如vue中实现未登录的用户无法访问系统资源等)
  2. gateway拦截到请求并检查请求头中是否携带token,有则放行没有则无权限无法访问(返回401)
  3. 客户端接拦截到响应并解析出当前响应状态码是401,则会redirect到登录页面(未登录的用户请先登录)gateway拦截到请求后判断当前是登录url则放行,转发到认证服务器进行登录操作
  4. 根据email / username判断是否存在数据库,存在则取出数据对登录密码进行加密比对,比对通过则代表成功登录生成token,并且获取该用户所对应角色的权限信息,并将其存在redis中
  5. 用户端拦截到登录响应数据,从其中获取到token和一些用户信息保存到本地(session,localStorage等)
  6. 登录后的每次请求发送前都会在请求头中添加token信息(本次实现鉴权逻辑不写在认证服务器中,由每个资源服务器引入jar包依赖各自鉴权
  7. 通过@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中统一返回就好了,这里包括了tokentokenExpire(便于前端判断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 的权限认证模式也是一种普遍的实现方案,对于上面自研认证服务毕竟还是没有实现高并发场景下的功能,所以有感兴趣的读者可以往这方面继续专研⛽

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/645327.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HTML跳动的爱心

目录 写在前面 HTML简介 跳动的爱心 代码分析 运行结果 推荐文章 写在后面 写在前面 哎呀&#xff0c;这是谁的小心心&#xff1f;跳得好快吖&#xff01; HTML简介 老生常谈啦&#xff0c;咱们还是从HTML开始吧&#xff01; HTML是超文本标记语言&#xff08;Hyper…

数据结构--二叉搜索树

目录 二叉搜索树的概念 二叉树的实现 结点类 函数接口总览 实现二叉树 二叉搜索树的应用 K模型 KV模型 二叉搜索树的性能分析 二叉搜索树的概念 二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称BST&#xff09;是一种特殊的二叉树&#xff0c;其具有以下几…

Installing Tinyproxy on CentOS 7 测试可用

Installing Tinyproxy on CentOS 7 For RHEL/CentOS 7 systems, Tinyproxy is part of EPEL (Extra Packages for Enterprise Linux). Install EPEL on CentOS 7 yum install epel-release -y yum update -y Install Tinyproxy on CentOS 7 yum install tinyproxy -y 编辑…

重开之数据结构(二刷)

引言: 由于前段时间学习效率不高,导致后面复习前面数据结构没有一个大纲,因此打算重新来学习以下数据结构,期望再次把数据结构学透,并有深刻的印象.并且记录每一次的学习记录 以便于后续复习 二分查找 需求:在有序数组arr内,查找target值 如果找到返回索引位置如果找不到返回…

使用python对指定文件夹下的pdf文件进行合并

使用python对指定文件夹下的pdf文件进行合并 介绍效果代码 介绍 对指定文件夹下的所有pdf文件进行合并成一个pdf文件。 效果 要合并的pdf文件&#xff0c;共计16个1页的pdf文件。 合并成功的pdf文件&#xff1a;一个16页的pdf文件。 代码 import os from PyPDF2 import …

3款简洁个人网站引导页(附带源码)

3款个人网站引导页 效果图及部分源码1.个人页2.引导页3.导航页 领取源码下期更新预报 效果图及部分源码 1.个人页 部分源码 * {margin: 0;padding: 0; }body {background-image: linear-gradient(to left, rgba(255, 0, 149, 0.2), rgba(0, 247, 255, 0.2)), url(../img/bg.j…

贪心算法4(c++)

过河的最短时间 题目描述 输入 在漆黑的夜里&#xff0c;N位旅行者来到了一座狭窄而且没有护栏的桥边。如果不借助手电筒的话&#xff0c;大家是无论如何也不敢过桥去的。不幸的是&#xff0c;N个人一共只带了一只手电筒&#xff0c;而桥窄得只够让两个人同时过&#xff0c;如果…

Spark搭建 Standalone模式详细步骤

Standalone模式概述&#xff1a; Standalone模式是Spark自带的一种集群模式&#xff08;本地集群&#xff0c;不依赖与外部集群&#xff0c;比如Yarn&#xff09;&#xff0c;可以真实地在多个机器之间搭建Spark集群的环境。 Standalone是完整的Spark运行环境,其中: Master角…

QGraphicsView实现简易地图16『爆炸效果』

前文链接&#xff1a;QGraphicsView实现简易地图15『测量面积』 一种简单的爆炸波扩散效果 动态演示效果&#xff1a; 静态展示图片&#xff1a; 核心代码&#xff1a; #pragma once #include "../AbstractGeoItem.h" #include "DataStruct/GeoData.h"…

Minecraft服务器如何搭建

Minecraft这是原版英文名称&#xff0c;在中国大陆被译为《我的世界》&#xff0c;这款游戏很火爆。台湾的很多小伙伴也在玩&#xff0c;其译名为《我的创世神》。现在这款游戏在国内已经被网易代理了。因为这款游戏开源&#xff0c;所以任何人都可以搭建服务器端&#xff0c;如…

Aloha机械臂的mujoco仿真问题记录

今天在测试ACT代码时&#xff0c;遇到了仿真中的机械臂无法摆放正确的姿势来抓去红色方块。 后来经过测试&#xff0c;发现应该是python包的版本问题有误&#xff0c;下面记录下正确的包版本&#xff1a; 官方给出的包&#xff1a; conda create -n aloha python3.8.10 conda…

LearnOpenGL(二十)之立方体贴图

一、创建立方体贴图 首先&#xff0c;生成一个纹理&#xff0c;并将其绑定到纹理目标GL_TEXTURE_CUBE_MAP&#xff1a; unsigned int textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); 因为立方体贴图包含有6个纹理&#xff0…

【Spring Boot】分层开发 Web 应用程序(含实例)

分层开发 Web 应用程序 1.应用程序分层开发模式&#xff1a;MVC1.1 了解 MVC 模式1.2 MVC 和三层架构的关系 2.视图技术 Thymeleaf3.使用控制器3.1 常用注解3.1.1 Controller3.1.2 RestController3.1.3 RequestMapping3.1.4 PathVariable 3.2 将 URL 映射到方法3.3 在方法中使用…

打卡信奥刷题(19)用Scratch图形化工具信奥B3972 [语言月赛 202405] 二进制 题解

进制转换是经典的编程题&#xff0c;尤其是10进制转换为2进制。方法是拿给定的数&#xff0c;不断地除2&#xff0c;将余数放在对应的位置&#xff0c;剩下的数为对应数除2向下取整 [语言月赛 202405] 二进制 题目描述 在介绍十进制转二进制的篇目中&#xff0c;我们总会看到…

GDPU JavaWeb mvc模式

搭建一个mvc框架的小实例。 简易计算器 有一个名为inputNumber.jsp的页面提供一个表单&#xff0c;用户可以通过表单输入两个数和运算符号提交给Servlet控制器&#xff1b;由名为ComputerBean.java生成的JavaBean负责存储运算数、运算符号和运算结果&#xff0c;由名为handleCo…

2024最新 Jenkins + Docker实战教程(二) - Jenkins相关配置

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

鸿蒙 DevEcoStudio:发布进度条通知

使用notificationManager及wantAgent实现功能import notificationManager from ohos.notificationManager import wantAgent from ohos.app.ability.wantAgent Entry Component struct Index {State message: string 发布进度条通知progressValue: number0async publicDownloa…

24李林跌落神坛,880还刷吗?还是换1000、900、660?

“李林今年跌落神坛了&#xff01;” “全是固定题型没新题&#xff0c;结果今年考的全是新题。” 880是“老真题的神”&#xff0c; 遇到24年&#xff0c;冷门考点多&#xff0c;计算量又大&#xff0c;就不灵了。 但“老真题”&#xff0c;还是得刷。就像往年真题是要刷的…

传输层——UDP

在学习计算机网络的过程中&#xff0c;我们知道OSI七层协议模型&#xff0c;但是在实际开发应 用中我们发现OSI七层协议模型并不适合实施&#xff0c;因为OSI上三层通常都是由开 发人员统一完成的&#xff0c;这三层之间在实现过程中没有一个明确的界限&#xff0c;所以我 们更…

001集—创建、写入、读取文件fileopen函数——vb.net

此实例为在指定路径下创建一个txt文本文件&#xff0c;在文本文件内输入文字&#xff0c;并弹窗显示输入文字&#xff0c;代码如下&#xff1a; Public Class Form1Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.ClickDim testcontent As Str…