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

news2024/11/17 16:34:56

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 的权限认证模式也是一种普遍的实现方案,对于上面自研认证服务毕竟还是没有实现高并发场景下的功能,所以有感兴趣的读者可以往这方面继续专研⛽

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

YOLOv5改进策略:Focaler-IoU损失函数改进

文章目录 1、前言2、摘要3、Focaler-IoU&#xff1a;4、代码实现5、目标检测系列文章 1、前言 ​ 目标检测是计算机视觉的基本任务之一&#xff0c;旨在识别图像中的目标并定位其位置。目标检测算法可分为基于锚点和无锚点的方法。基于锚点的方法包括Faster R-CNN、YOLO系列、…

数据结构 —— 栈 与 队列

1.栈 1.1栈的结构和概念 栈&#xff08;Stack&#xff09;是一种特殊的线性数据结构&#xff0c;它遵循后进先出&#xff08;LIFO&#xff0c;Last In First Out&#xff09;的原则。栈只允许在一端插入和删除数据&#xff0c;这一端被称为栈顶&#xff08;top&#xff09;&a…

Hudi 多表摄取工具 HoodieMultiTableStreamer 配置方法与示例

博主历时三年精心创作的《大数据平台架构与原型实现&#xff1a;数据中台建设实战》一书现已由知名IT图书品牌电子工业出版社博文视点出版发行&#xff0c;点击《重磅推荐&#xff1a;建大数据平台太难了&#xff01;给我发个工程原型吧&#xff01;》了解图书详情&#xff0c;…

基于SpringBoot和Mybatis实现的留言板案例

目录 一、需求及界面展示 二、准备工作 引入依赖 .yml文件相关配置 数据库数据准备 三、编写后端代码 需求分析 代码结构 Model Mapper Service Controller 前端代码 四、测试 一、需求及界面展示 需求&#xff1a; 1. 输入留言信息&#xff0c;点击提交&…

2024-6-遥远的救世主

2024-6-遥远的救世主 2024-4-18 豆豆 fatux&#xff1a; 2021.5.26 看完电视剧《天道》之后购买本书&#xff0c;断断续续一直没有读完。 非常好奇&#xff0c;一个什么样的作者能写出如此奇书。老丁&#xff0c;一个智者&#xff0c;智者是多么孤独&#xff0c;因为找不到同…

AtCoder Regular Contest 178 A~D

A.Good Permutation 2&#xff08;贪心&#xff09; 题意&#xff1a; 给你一个正整数 N N N和一个由 M M M个正整数 A ( A 1 , A 2 , … , A M ) A(A_{1},A_{2}, \dots,A_{M}) A(A1​,A2​,…,AM​)组成的序列。 在这里&#xff0c; A A A的所有元素都是介于 1 1 1和 N N …

nss做题

[NCTF 2018]签到题 1.f12在index.php中找到flag [NSSCTF 2022 Spring Recruit]ezgame 1.在js源码中就有flag [UUCTF 2022 新生赛]websign 1.打开环境后发现ctrlu和右键&#xff0c;f12都被禁用了。两种方法&#xff0c;第一种&#xff1a;禁用js&#xff1b;第二中提前打开…

数据结构(五)树与二叉树

2024年5月26日一稿(王道P142) 基本概念 术语 性质 二叉树 5.2.2 二叉树存储结构

vue3中基于element-plus封装一个表格弹框组件,要求可以单选和多选table数据

单选&#xff1a; <template><SelectMaterialref"selectMaterialRef"check"checkbox"select"selectMaterial"></SelectMaterial><el-button type"primary" size"small" icon"el-icon-plus"…

【STM32】新建工程(江科大)

文章目录 STM32的开发方式库函数文件夹一、新建一个基于标准库的工程1.建立一个存放工程的文件夹2.打开Keil5 二、通过配置寄存器来完成点灯1.配置RCC寄存器2.配置PC13口&#xff08;1&#xff09;配置PC13口的模式&#xff08;2&#xff09;给PC13口输出数据 三、为寄存器添加…

与MySQL的初相遇

&#x1f30e;初识MySQL 注&#xff1a;本文SQL语句只为了验证猜想&#xff0c;不会也不要紧。 文章目录&#xff1a; MySql开端 认识数据库       什么是数据库       主流数据库       MySQL的本质 MySQL基础使用       连接mysql服务器     …

【Linux初探】:解锁开源世界的神秘钥匙

文章目录 &#x1f680;一、了解Linux&#x1f525;二、Linux 的发行版❤️三、Linux应用领域&#x1f4a5;四、Linux vs Windows & mac &#x1f680;一、了解Linux Linux是一种自由、开放源代码的操作系统&#xff0c;它的内核由芬兰计算机科学家Linus Torvalds在1991年创…

图片AI高效生成惊艳之作,一键解锁无限创意,轻松打造概念艺术新纪元!

在数字化时代&#xff0c;图片已经成为我们表达创意、传递信息的重要载体。然而&#xff0c;传统的图片生成方式往往耗时耗力&#xff0c;无法满足我们对于高效、创意的需求。幸运的是&#xff0c;现在有了图片AI&#xff0c;它以其高效、智能的特点&#xff0c;为我们带来了全…

数组-最接近给出数字的三数之和

题目描述 解题思路 这里使用三层for循环&#xff0c;暴力解法穷举所有三个数和的可能性&#xff0c;注意三层循环里的索引不要重复。 代码实现 import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返…

C语言——小知识和小细节19

一、奇数位与偶数位互换 1、题目介绍 实现一个宏&#xff0c;将一个整数的二进制补码的奇数位与偶数位互换。输出格式依旧是十进制整数。示例&#xff1a; 2、分析 既然想要交换奇数位和偶数位上的数字&#xff0c;那么我们就要先得到奇数位和偶数位上的数字&#xff0c;那么…

THREE.JS中的向量点乘,以及他的几何意义。

1. THREE.JS中的向量点乘&#xff0c;以及他的几何意义 向量点乘的公式 : 2. 在three.js 中计算向量点乘 const a new THREE.Vector3(10, 10, 0); const b new THREE.Vector3(20, 0, 0); const dot a.dot(b);从这里可以看出&#xff0c;向量的点乘的结果是一个数字(标量…

嵌入式进阶——LED呼吸灯(PWM)

&#x1f3ac; 秋野酱&#xff1a;《个人主页》 &#x1f525; 个人专栏:《Java专栏》《Python专栏》 ⛺️心若有所向往,何惧道阻且长 文章目录 PWM基础概念STC8H芯片PWMA应用PWM配置详解占空比 PWM基础概念 PWM全称是脉宽调制&#xff08;Pulse Width Modulation&#xff09…

安卓手机电脑平板均支持

最近随着人工智能的火热&#xff0c;越来越多人问我怎么设置&#xff0c;我这边主要提供简单的配置&#xff0c;能够实现想要的功能&#xff0c;不懂得的友友们可以私聊我&#xff0c;

一文读懂Apollo客户端配置加载流程

本文基于 apollo-client 2.1.0 版本源码进行分析 Apollo 是携程开源的配置中心&#xff0c;能够集中化管理应用不同环境、不同集群的配置&#xff0c;配置修改后能够实时推送到应用端&#xff0c;并且具备规范的权限、流程治理等特性。 Apollo支持4个维度管理Key-Value格式的配…

MyBatis详细教程!!(入门版)

目录 什么是MyBatis&#xff1f; MyBatis入门 1&#xff09;创建工程 2&#xff09;数据准备 3&#xff09;配置数据库连接字符串 4&#xff09;写持久层代码 5&#xff09;生成测试类 MyBatis打印日志 传递参数 MyBatis的增、删、改 增&#xff08;Insert&#xff0…