Spring Boot 优雅集成 Spring Security 5.7(安全框架)与 JWT(双令牌机制)

news2024/11/16 2:50:41

Spring Boot 集成 Spring Security (安全框架)

本章节将介绍 Spring Boot 集成 Spring Security 5.7(安全框架)。

🤖 Spring Boot 2.x 实践案例(代码仓库)

介绍

Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。

它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IOC(控制反转),DI(依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

认证和授权作为 Spring Security 安全框架的核心功能:

认证(Authentication):验证当前访问系统用户是否是本系统用户,并且要确认具体是哪个用户。

认证流程

授权(Authorization):经过认证后判断当前用户是否具有权限进行某个操作。

授权流程

快速开始

引入依赖

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JWT -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.4.0</version>
</dependency>
<!-- Lombok 插件 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

配置文件

# 开发环境配置

server:
  # 服务端口
  port: 8081

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/security?useUnicode=true&useSSL=false&serverTimezone=UTC&characterEncoding=UTF8&nullCatalogMeansCurrent=true
    username: "root"
    password: "88888888"
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    password: 88888888

security:
  # 密钥
  secret: spring-boot-learning-examples
  # 访问令牌过期时间(1天)
  access-expires: 86400
  # 刷新令牌过期时间(30天)
  refresh-expires: 2592000
  # 白名单
  white-list: /user/login,/user/register,/user/refresh

测试登录

启动项目后,尝试访问某个接口,会自动跳转到 Spring Security 默认登录页面。

默认登录页

默认退出页

默认用户名:user

默认密码:启动项目时会随机生成密码并输出在控制台中:

Using generated security password: 0b7bb972-ab4c-461c-ab19-7824d23d9b87

默认密码

认证

基于数据库加载用户

Spring Security 默认从内存加载用户,需要实现从数据库加载并校验用户。

具体步骤

1)创建 UserServiceImpl

2)实现 UserDetailsService 接口

3)重写 loadUserByUsername 方法

4)根据用户名校验用户并查询用户相关权限信息(授权)

5)将数据封装成 UserDetails(创建类并实现该接口) 并返回

核心代码

LoginUser

@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LoginUser implements UserDetails {
    /**
     * 用户编号
     */
    private Long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    @JsonIgnore
    private String password;

    /**
     * 权限集合
     */
    @JsonIgnore
    private List<SimpleGrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

注意:需添加 @JsonIgnore 注解,否则会出现序列化失败问题

UserServiceImpl

@Service
public class UserServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        UserDO user = getUserByUsername(username);
        // TODO 查询用户权限信息
        return LoginUser.builder()
                .id(user.getId())
                .username(user.getUsername())
                .password(user.getPassword())
                .build();
    }

    @Override
    public UserDO getUserByUsername(String username) {
        LambdaQueryWrapper<UserDO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UserDO::getUsername, username);
        Optional<UserDO> optional = Optional.ofNullable(baseMapper.selectOne(queryWrapper));
        return optional.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
    }
}

注意:如需测试,需要往用户表中写入数据,并且如果用户密码想要明文存储,需要在密码前加 {noop}

密码加密存储

实际项目中,密码不会以明文形式存储在数据库中,而 Spring Security 密码校验器要求数据库中密码格式为:{id}password,而默认使用 NoOpPasswordEncoder 加密器,此方式不会对密码进行加密处理,所以不推荐这种形式。

本项目将使用 Spring Security 提供的 BCryptPasswordEncoder 来进行密码校验。

具体步骤

1)创建 Spring Security Bean 配置类(避免循环依赖问题)

2)继承 WebSecurityConfigurerAdapter(旧用法)

3)将 BCryptPasswordEncoder 对象注入 Spring 容器中

注意:Spring Security 5.7.x 版本配置方式与以往有所不同,WebSecurityConfigurerAdapter 在 Spring Security 5.7 版本中已被标记 @Deprecated,未来这个类将被移除,本教程将使用继承 WebSecurityConfigurerAdapter 方式来实现 Spring Security 配置,但在实际代码中配置采用最新版本方式!

Spring Security 版本配置区别如下:

1)Spring Boot 2.7.0 版本之前,需要写个配置类继承 WebSecurityConfigurerAdapter,然后重写 Adapter 中方法进行配置;

2)Spring Boot 2.7.0 版本之后无需再继承 WebSecurityConfigurerAdapter,只需直接声明配置类,再配置一个生成 SecurityFilterChainBean 方法,把原来 HttpSecurity 配置移动到该方法中即可。

用的挺顺手的 Spring Security 配置类,居然就要被官方弃用了!

核心代码

@Configuration
public class CommonSecurityConfiguration {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

注意:同一密码每次加密后生成密文互不相同,因此需使用 matches() 方法来进行比较。

@SpringBootTest
@Slf4j
class SecurityApplicationTests {

    @Test
    void passwordEncoder() {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123456");
        log.info("加密密文:{}", password);
        boolean matches = passwordEncoder.matches("123456", password);
        log.info("是否匹配:{}", matches);
    }

}

BCryptPasswordEncoder

登录接口

具体步骤

1)创建 Spring Security 配置类

2)生成 SecurityFilterChain Bean 方法

3)放行登录接口

4)注入 AuthenticationManager 认证管理器

5)用户认证

6)生成JWT令牌并返回(双令牌机制)

7)访问令牌(AccessToken)存入 Redis 缓存

注意:Spring Security 5.7.x 版本配置方式与以往有所不同,WebSecurityConfigurerAdapter 在 Spring Security 5.7 版本中已被标记 @Deprecated,未来这个类将被移除,所以本教程将采用最新版本配置方式!

Spring Security 版本配置区别如下:

1)Spring Boot 2.7.0 版本之前,需要写个配置类继承 WebSecurityConfigurerAdapter,然后重写 Adapter 中方法进行配置;

2)Spring Boot 2.7.0 版本之后无需再继承 WebSecurityConfigurerAdapter,只需直接声明配置类,再配置一个生成 SecurityFilterChainBean 方法,把原来 HttpSecurity 配置移动到该方法中即可。

用的挺顺手的 Spring Security 配置类,居然就要被官方弃用了!

核心代码

1)放行登录接口

需要自定义登陆接口,让 Spring Security 对登录接口放行,之后用户访问该接口时,不用登录也能访问:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
      httpSecurity
              // 过滤请求
              .authorizeRequests()
              // 接口放行
              .antMatchers("/user/login").permitAll()
              // 除上面外的所有请求全部需要鉴权认证
              .anyRequest()
              .authenticated()
              .and()
              // CSRF禁用
              .csrf().disable()
              // 禁用HTTP响应标头
              .headers().cacheControl().disable()
              .and()
              // 基于JWT令牌,无需Session
              .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS));
      return httpSecurity.build();
  }
}

2)注入 AuthenticationManager 认证管理器

由于在登录接口中,需通过 AuthenticationManager 接口中的 authenticate 方法来进行用户认证,所以需要在 CommonSecurityConfiguration 配置文件中注入 AuthenticationManager 接口。

@Configuration
public class CommonSecurityConfiguration {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
      return authenticationConfiguration.getAuthenticationManager();
    }
}

3)用户认证

调用 AuthenticationManager 接口中的 authenticate 方法来进行用户认证,该方法需传入 Authentication ,由于 Authentication 是接口,因此需要传入它的实现类。

由于登录方式采用账号密码形式,所以需使用 UsernamePasswordAuthenticationToken 实现类,此类需传入用户名(principal)和密码(credentials)。

认证成功时,Spring Security 将返回 Authentication ,内容如下:

authentication

注意:Authentication 为 NULL 时,说明认证没通过,要么没查询到这个用户,要么密码比对不通过。

此时还需生成 JWT 令牌,将其放入响应中返回,为了能够实现双令牌机制需将访问令牌存入 Redis 缓存中。

@RestController
@RequestMapping("/user")
@Validated
public class UserController {

    @Autowired
    private UserService userService;
  
    @Autowired
    private RedisUtil redisUtil;
  
    @Autowired
    private AuthenticationManager authenticationManager;
  
    @PostMapping("/login")
    public ResponseVO<TokenVO> login(@RequestBody @Validated LoginDTO dto) {
        authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword()));
        UserDO user = userService.getUserByUsername(dto.getUsername());
        TokenVO token = JwtUtil.generateTokens(user.getUsername());
        redisUtil.set("user:token:" + user.getUsername() + ":string", token.getAccessToken(), JwtUtil.getAccessExpires());
        return ResponseVO.success("登录成功", token);
    }
}

认证过滤器

具体步骤

1)接口白名单放行

2)从请求头中解析令牌

3)判断令牌是否存在于黑名单中

4)从 Redis 获取令牌

5)校验令牌是否合法或有效

6)存入 SecurityContextHolder

7)配置过滤器顺序

核心代码

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
  
    @Autowired
    private UserDetailsService userDetailsService;
  
    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
        if (Arrays.stream(JwtUtil.getWhiteList()).anyMatch(uri -> uri.equals(request.getServletPath()))) {
          filterChain.doFilter(request, response);
          return;
        }
        String token = JwtUtil.decodeTokenFromRequest(request);
        // 判断令牌是否存在黑名单中
        if (redisUtil.hasKey("token:black:" + JwtUtil.getJti(token) + ":string")) {
          throw new RuntimeException(Code.TOKEN_INVALID.getZhDescription());
        }
        String username = JwtUtil.getUsername(token);
        if (StringUtils.hasText(username) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (!StringUtils.hasText(redisUtil.get("user:token:" + username + ":string"))) {
                throw new RuntimeException(Code.ACCESS_TOKEN_EXPIRED.getZhDescription());
            }
            // 校验令牌是否有效
            try {
                JwtUtil.decodeAccessToken(token);
                JwtUtil.checkTokenValid(token, userDetails.getUsername());
            } catch (TokenExpiredException e) {
                // TODO 全局异常处理
                throw new RuntimeException(Code.ACCESS_TOKEN_EXPIRED.getZhDescription());
            } catch (JWTVerificationException e) {
                throw new RuntimeException(Code.TOKEN_INVALID.getZhDescription());
            }
            // 权限信息
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}

注意:该过滤器实现接口并不是之前的Filter,而是去继承 OncePerRequestFilter(过滤器抽象类),通常被用于继承实现并在每次请求时只执行一次过滤)。

在配置文件中,将过滤器加到 UsernamePasswordAuthenticationFilter 前面:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
    
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 过滤请求
                .authorizeRequests()
                // 静态资源放行
                .antMatchers(STATIC_RESOURCE_WHITE_LIST).permitAll()
                // 接口放行
                .antMatchers(JwtUtil.getWhiteList()).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest()
                .authenticated()
                .and()
                // CSRF禁用
                .csrf().disable()
                // 禁用HTTP响应标头
                .headers().cacheControl().disable()
                .and()
                // 基于JWT令牌,无需Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 拦截器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }
}

退出登录

JWT最大优势在于它是无状态,自身包含了认证鉴权所需要的所有信息,服务器端无需对其存储,从而给服务器减少了存储开销。

但是无状态引出的问题也是可想而知的,它无法作废未过期的JWT。举例说明注销场景下,就传统的cookie/session认证机制,只需要把存在服务器端的session删掉就OK了。

但是JWT呢,它是不存在服务器端的啊,好的那我删存在客户端的JWT行了吧。额,社会本就复杂别再欺骗自己了好么,被你在客户端删掉的JWT还是可以通过服务器端认证的。

使用JWT要非常明确一点:JWT失效唯一途径就是等待时间过期

本教程借助黑名单方案实现JWT失效:

退出登录时,将访问令牌放入 Redis 缓存中,并且设置过期时间为访问令牌过期时间;请求资源时判断该令牌是否在 Redis 中,如果存在则拒绝访问。

具体步骤

1)全局过滤器中需要判断黑名单是否存在当前访问令牌

2)解析请求头中令牌(JTIEXPIRES_AT

3)将JTI字段作为键存放到 Redis 缓存中,并设置访问令牌过期时间

4)清除认证信息

5)配置退出登录接口与处理器

实战!退出登录时如何借助外力使JWT令牌失效?

核心代码

@Service
@RequiredArgsConstructor
public class LogoutHandler implements org.springframework.security.web.authentication.logout.LogoutHandler {

    @Autowired
    private RedisUtil redisUtil;
  
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String token = JwtUtil.decodeTokenFromRequest(request);
        blacklist(token);
        SecurityContextHolder.clearContext();
    }
  
    /**
     * 加入黑名单
     *
     * @param token 令牌
     */
    private void blacklist(String token) {
        String jti = JwtUtil.getJti(token);
        Long expires = JwtUtil.getExpires(token);
        redisUtil.set("token:black:" + jti + ":string", StringConstant.EMPTY, DateUtil.minusSeconds(expires));
    }
}

退出登录成功处理器:

@Component
public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {

  @Override
  public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
      SecurityContextHolder.clearContext();
      response.setHeader("Access-Control-Allow-Origin", "*");
      response.setHeader("Cache-Control", "no-cache");
      response.setContentType("application/json");
      response.setCharacterEncoding("UTF-8");
      response.setStatus(HttpStatus.OK.value());
      response.getWriter().println(GenericJacksonUtil.objectToJson(ResponseVO.success()));
      response.getWriter().flush();
  }
}

在 Spring Security 配置文件中配置退出登录接口与处理器:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
    
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private LogoutHandler logoutHandler;

    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 过滤请求
                .authorizeRequests()
                // 静态资源放行
                .antMatchers(STATIC_RESOURCE_WHITE_LIST).permitAll()
                // 接口放行
                .antMatchers(JwtUtil.getWhiteList()).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest()
                .authenticated()
                .and()
                // CSRF禁用
                .csrf().disable()
                // 禁用HTTP响应标头
                .headers().cacheControl().disable()
                .and()
                // 基于JWT令牌,无需Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 拦截器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                // 退出登录
                .logout()
                .logoutUrl("/user/logout")
                .logoutSuccessHandler(logoutSuccessHandler)
                .addLogoutHandler(logoutHandler);
        return httpSecurity.build();
    }
}

授权

在 Spring Security 中,会使用 FilterSecurityInterceptor(默认) 来进行权限校验。

FilterSecurityInterceptor 中会从 SecurityContextHolder 获取其中的 AuthenticationAuthentication 包含权限信息,用来判断当前用户是否拥有访问当前资源所需的权限。

具体步骤

1)开启权限注解

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
}

开启之后,在需要权限才能访问的接口上打上 @PreAuthorize 注解即可。

2)查询用户权限信息(见核心代码)

3)封装权限信息

重写 loadUserByUsername 方法时,查询出用户后,还需将用户对应的权限信息,封装到之前定义的 UserDetails 的实现类 LoginUser 并返回:

@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LoginUser implements UserDetails {
    /**
     * 用户编号
     */
    private Long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 菜单集合
     */
    private List<MenuDO> menuList;
  
    /**
     * 权限集合
     */
    @JsonIgnore
    private List<SimpleGrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (Objects.nonNull(authorities)) {
          return authorities;
        }
        return menuList.stream()
                .filter(menu -> StringUtils.hasText(menu.getPermission()))
                .map(menu -> new SimpleGrantedAuthority(menu.getPermission()))
                .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
@Service
public class UserServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        UserDO user = getUserByUsername(username);
        // 查询用户菜单列表
        List<MenuDO> menuList = listUserPermissions(user.getId());
        // 查询用户权限信息
        return new LoginUser(user, menuList);
    }
}

核心代码

<select id="listMenusByRoleIds" resultType="com.starimmortal.security.pojo.MenuDO">
    SELECT t1.id, t1.parent_id, t1.`name`, t1.`path`, t1.permission, t1.`icon`, t1.component, t1.`type`, t1.`visible`, t1.`status`, t1.keep_alive, t1.sort_order, t1.create_time, t1.update_time, t1.is_deleted
    FROM `sys_menu` AS t1
    JOIN sys_role_menu AS t2 ON t1.id = t2.menu_id
    WHERE t1.is_deleted = 0 AND t1.`status` = 0
    AND t2.role_id IN
    <foreach collection="roleIds" item="roleId" index="index" open="(" separator="," close=")">
      #{roleId}
    </foreach>
    GROUP BY t1.id
</select>

权限控制

基于方法注解

Spring Security 默认是关闭方法注解,开启它只需要通过引入 @EnableGlobalMethodSecurity 注解即可:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
}

@EnableGlobalMethodSecurity 提供了以下三种方式:

1)prePostEnabled:基于表达式(Spring EL)注解:

  • @PreAuthorize:进入方法之前验证授权:

    @PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
    

    表示方法执行之前,判断方法参数值是否等于 principal 中保存的参数值;或者当前用户是否具有 ROLE_ADMIN 权限,两者符合其中一种即可访问该方法,内置如下方法:

    • hasAuthority:只能传入一个权限,只有用户有这个权限才可以访问资源;

    • hasAnyAuthority:可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源;

    • hasRole:要求有对应角色才可以访问,但是它内部会把传入的参数拼接上 ROLE_ 后再去比较:

      @PreAuthorize("hasRole('system:dept:list')")
      

      注意:用户有 system:dept:list 权限是无法访问的,得有 ROLE_system:dept:list 权限才可以。

    • hasAnyRole:有任意角色即可访问。

  • @PostAuthorize:检查授权方法之后才被执行并且可以影响执行方法的返回值:

    @PostAuthorize("returnObject.username == authentication.principal.nickName")
    public CustomUser loadUserDetail(String username) {
        return userRoleRepository.loadUserByUserName(username);
    }
    
  • @PostFilter:在方法执行之后执行,而且这里可以调用方法的返回值,然后对返回值进行过滤或处理或修改并返回。

  • @PreFilter:在方法执行之前执行,而且这里可以调用方法的参数,然后对参数值进行过滤或处理或修改。

2)securedEnabled:开启基于角色注解:

@Secured("ROLE_VIEWER")
public String getUsername() {}

@Secured({ "ROLE_DBA", "ROLE_ADMIN" })
public String getNickname() {}

@Secured(“ROLE_VIEWER”):只有拥有 ROLE_VIEWER 角色的用户,才能够访问;

@Secured({ “ROLE_DBA”, “ROLE_ADMIN” }):拥有 "ROLE_DBA", "ROLE_ADMIN" 两个角色中的任意一个角色,均可访问。

注意:@Secured 注解不支持 Spring EL 表达式!

3)jsr250Enabled:开启对JSR250注解:

  • @DenyAll:拒绝所有权限

  • @RolesAllowed:在功能及使用方法上与 @Secured 完全相同

  • @PermitAll:接受所有权限

基于配置文件

方法名称方法作用
permitAll()表示所匹配的URL任何人都允许访问
anonymous()表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为anonymous()的url会执行filterChain中的filter
denyAll()表示所匹配的URL都不允许被访问。
authenticated()表示所匹配的URL都需要被认证才能访问
rememberMe()允许通过remember-me登录的用户访问
access()SpringEl表达式结果为true时可以访问
fullyAuthenticated()用户完全认证可以访问(非remember-me下自动登录)
hasRole()如果有参数,参数表示角色,则其角色可以访问
hasAnyRole()如果有参数,参数表示角色,则其中任何一个角色可以访问
hasAuthority()如果有参数,参数表示权限,则其权限可以访问
hasAnyAuthority()如果有参数,参数表示权限,则其中任何一个权限可以访问
hasIpAddress()如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问

自定义异常处理

在 Spring Security 中,认证或者授权的过程中出现异常会被 ExceptionTranslationFilter 捕获,在 ExceptionTranslationFilter 中会去判断是认证失败还是授权失败出现的异常。

1)自定义认证失败异常

@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Cache-Control", "no-cache");
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().println(GenericJacksonUtil.objectToJson(ResponseVO.error(Code.UN_AUTHORIZATION.getCode(), Code.UN_AUTHORIZATION.getZhDescription(), authException.getMessage())));
        response.getWriter().flush();
    }
}

2)自定义授权失败异常

@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Cache-Control", "no-cache");
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.getWriter().println(GenericJacksonUtil.objectToJson(ResponseVO.error(Code.UN_AUTHENTICATION.getCode(), Code.UN_AUTHENTICATION.getZhDescription(), accessDeniedException.getMessage())));
        response.getWriter().flush();
    }
}

3)Spring Security 配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Autowired
    private RestAccessDeniedHandler restAccessDeniedHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 过滤请求
                .authorizeRequests()
                // 静态资源放行
                .antMatchers(STATIC_RESOURCE_WHITE_LIST).permitAll()
                // 接口放行
                .antMatchers(JwtUtil.getWhiteList()).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest()
                .authenticated()
                .and()
                // CSRF禁用
                .csrf().disable()
                // 禁用HTTP响应标头
                .headers().cacheControl().disable()
                .and()
                // 基于JWT令牌,无需Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 认证与授权失败处理类
                .exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint)
                .accessDeniedHandler(restAccessDeniedHandler)
                .and()
                // 拦截器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }
}

跨域

浏览器出于安全考虑,使用 XMLHttpRequest 对象发起 HTTP 请求时必须遵守同源策略,否则就是跨域的 HTTP 请求,默认情
况下是被禁止的,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

1)Spring Boot 跨域配置

@Configuration(proxyBeanMethods = false)
public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }

}

2)Spring Security 跨域配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
              // 跨域
              .cors()
              .and()
              .headers().frameOptions().disable();
        return httpSecurity.build();
    }
}

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

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

相关文章

【CSDN创作纪念日】——博客小梦的“256”鸭~

博客小梦的创作纪念日&#x1f60e; 前言&#x1f64c;与CSDN的相遇浑水摸鱼的日常CSDN上的小小收获收获了 一群热爱编程&#xff0c;热爱创作的CSDN挚友创作上的小荣誉 憧憬未来 总结撒花&#x1f49e; &#x1f60e;博客昵称&#xff1a;博客小梦 &#x1f60a;最喜欢的座右铭…

【Spring】— Spring MVC简单数据绑定(一)

目录 Spring MVC数据绑定1.数据绑定概述2.简单数据绑定2.1 绑定默认数据类型2.2 绑定简单数据类型 Spring MVC数据绑定 1.数据绑定概述 在执行程序时&#xff0c;Spring MVC根据客户端请求参数的不同将请求消息中的信息以一定的方式转换并绑定到控制器类的方法参数中。这种将…

基于VMD-LSTM-IOWA-RBF的碳排放混合预测研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

第J3-1周:DenseNet算法 实现乳腺癌识别

目录 一、理论基础1.前言2.设计理念3.网络结构4.与其他算法效果对比 二、 前期准备1. 设置GPU2. 导入数据3. 划分数据集 三、搭建网络模型1. DenseLayer模块2. DenseBlock模块3. Transition模块4. 构建DenseNet5. 构建densenet121 四、 训练模型1. 编写训练函数2. 编写测试函数…

I/O多路复用+高性能网络模式

前言&#xff1a; 本篇文章将介绍客户端-服务端之间从最简单的Socket模型到I/O多路复用的模式演变过程&#xff0c;并介绍Reactor和Proactor两种高性能网络模式 文章内容摘自&#xff1a;小林Coding I/O多路复用高性能网络模式 . 传统Socket模型传统Socket模型的性能瓶颈多进程…

【Java基础学习打卡12】Java入门程序

目录 前言一、Java程序开发运行流程二、Java程序源代码编写三、Java程序源代码编译四、Java程序运行五、Java入门程序问题总结 前言 本文首先介绍Java程序开发运行基础流程&#xff0c;然后先进行程序源代码编写&#xff0c;然后对Java程序代码进行编译&#xff0c;最后要运行…

Python学习笔记(1)--环境搭建,开发工具PyCharm 安装及初步使用

传送门>B站黑马python入门教程 目录 1.Python环境安装搭建安装python基础包验证安装文件 2.hello world3.开发工具PyCharm 安装及初步使用安装基础设置 1.Python环境安装搭建 安装python基础包 首先,打开python 官网 https://www.python.org/ 下载windows版 下载后进行安装 …

Triton教程 --- 解耦后端和模型

Triton教程 — 解耦后端和模型 Triton系列教程: 快速开始利用Triton部署你自己的模型Triton架构模型仓库存储代理模型设置优化动态批处理速率限制器模型管理自定义算子 解耦后端和模型 Triton 可以支持为一个请求发送多个响应或为一个请求发送零个响应的后端和模型。 解耦的…

论文笔记--Prompt Consistency for Zero-Shot Task Generalization

论文笔记--Prompt Consistency for Zero-Shot Task Generalization 1. 文章简介2. 文章概括3 文章重点技术3.1 Prompt-based zero-shot task generalization3.2 Prompt Consistency Training3.3 如何防止遗忘和退化&#xff1f; 4. 文章亮点5. 原文传送门 1. 文章简介 标题&am…

【numpy模块上}——数据分析01

目录索引 介绍&#xff1a;用处与特点&#xff1a;构成&#xff1a;导包&#xff1a;创建数组&#xff1a; numpy常用方法&#xff1a;常用属性查看&#xff1a;*获取秩的大小&#xff1a;**获取数组形状&#xff1a;**获取元素个数&#xff1a;**获取元素类型&#xff1a;**获…

行为型设计模式10-解释器模式

&#x1f9d1;‍&#x1f4bb;作者&#xff1a;猫十二懿 ❤️‍&#x1f525;账号&#xff1a;CSDN 、掘金 、个人博客 、Github &#x1f389;公众号&#xff1a;猫十二懿 解释器模式 1、解释器模式介绍 解释器模式&#xff08;Interpreter Pattern&#xff09;是一种行为设…

Kafka系列之:对源连接器的的Exactly-Once支持

Kafka系列之&#xff1a;对源连接器的的Exactly-Once支持 一、背景二、目标三、公共接口四、连接器 API 扩展五、REST API验证六、新指标七、计划变更八、任务计数记录九、重新平衡的准备十、源任务启动十一、领导者访问配置主题十二、用于隔离事务生产者的管理 API十三、解决了…

论文阅读 - SegFormer

文章目录 1 概述2 模型说明2.1 总体结构2.2 Hierarchical Transformer Encoder2.3 Lightweight All-MLP Decoder 3 SegFormer和SETR的比较参考资料 1 概述 图像分割任务和图像分类任务是非常相关的&#xff0c;前者是像素级别的分类&#xff0c;后者是图像级别的分类。基于分类…

不到3000块,搭建IT人的实验平台!性能可媲美服务器!

作为IT从业者&#xff0c;特别是运维这个岗位&#xff0c;没有自己的实验平台真的特别难受&#xff0c;那么如何搭建自己的实验平台呢&#xff1f;这是我最近思考并付诸实践的一个事情&#xff0c;最终找到了自己觉得比较可以的方案。 01 我的需求是什么&#xff1f; 大内存容量…

TypeScript——类(class)

ES6 中类的用法 下面我们先回顾一下 ES6 中类的用法&#xff0c;更详细的介绍可以参考 ECMAScript 6 入门 - Class 属性和方法 使用 class 定义类&#xff0c;使用 constructor 定义构造函数。 通过new生成新实例的时候&#xff0c;会自动调用构造函数。 class Person{con…

leetcode877. 石子游戏(动态规划-java)

石子游戏 leetcode877. 石子游戏题目描述暴力递归代码演示 动态规划 动态规划专题: leetcode877. 石子游戏 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/stone-game 题目描述 Alice 和 Bob 用几堆石子在做游戏。一共有…

HTTP与Fiddler使用

HTTP与Fiddler使用 HTTP与Fiddler使用FidderHTTP的报文结构&#xff1a;其他请求头User-agentReferer和OrigincookieHTTP状态码 HTTP与Fiddler使用 HTTP协议是使用十分广泛的应用层协议&#xff0c;也是一个可以由程序员进行设置的一个协议。该协议的结构规定了浏览器与万维网…

【C++】通讯录的基本实现,附有源码分享

目录 1、运行环境 2、系统实现功能 2.1菜单功能 2.2退出通讯录功能 2.3添加联系人功能 2.4显示联系人功能 2.5删除联系人功能 2.6查找联系人功能 2.7修改联系人功能 2.8清空联系人功能 2.9动态扩容功能 2.10选择优化功能 2.11文件操作 3、源码分享 1、运行环境 …

【备战秋招】每日一题:2023.04.26-华为OD机式-第三题-MC方块

在线评测链接:P1231 题目内容 MC最新版本更新了一种特殊的方块&#xff0c;幽匿催发体。这种方块能够吸收生物死亡掉落的经验并感染周围方块&#xff0c;使其变成幽匿块。Steve想要以此为基础尝试搭建一个经验仓库&#xff0c;他来到了创造超平坦模式&#xff0c;在只有草方块…

【微信小程序开发】第 7 课 - 小程序的常用组件

欢迎来到博主 Apeiron 的博客&#xff0c;祝您旅程愉快 &#xff01; 时止则止&#xff0c;时行则行。动静不失其时&#xff0c;其道光明。 目录 1、缘起 2、小程序中组件的分类 3、常用的视图容器类组件 3.1、view 组件 3.2、scroll - view 组件 3.3、swiper 和 swiper…