Spring Boot 优雅集成 Spring Security 5.7(安全框架)

news2024/11/25 0:22:39

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/648326.html

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

相关文章

为行业变革注入新动能,行易道入选“高工智能汽车智驾榜单”

6月8日到9日&#xff0c;2023高工智能汽车开发者大会在上海成功举行。与会期间&#xff0c;北京行易道科技有限公司&#xff08;以下简称“行易道”&#xff09;营销副总裁袁泽雁带来了以“车载毫米波雷达进入成像时代”主题演讲&#xff0c;为大家分享了4D毫米波雷达如何以“新…

Java网络开发(Tomcat异步分页+增删改查)——从同步到异步 从jsp 到 js + axios + vue 实现 数据分页显示 数据增删改查

目录 引出一些固定的东西1.固定的响应格式2.name 变成 v-model 进行双向绑定3.下拉框选中--:value"type.id" v-model"companyDb.typeId"4.vue导包固定写法5.script固定写法6.axios的get请求7.axios的post请求---let params new URLSearchParams()8.前端美…

MYSQL 在优化器缺陷在次验证,与MYSQL 熄火了 还是 成熟了??

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

聊聊我做 NeRF-3D重建性能优化经历

我们新推出大淘宝技术年度特刊《长期主义&#xff0c;往往从一些小事开始——工程师成长总结专题》&#xff0c;专题收录多位工程师真诚的心路历程与经验思考&#xff0c;覆盖终端、服务端、数据算法、技术质量等7大技术领域&#xff0c;欢迎一起沟通交流。 本文为此系列第四篇…

飞桨携手第二届GitLink开源夏令营,邀你参与顶尖开源项目!

想参与顶尖开源项目开发&#xff1f; 想熟悉开源社区参与流程&#xff1f; 想获得资深导师指导和丰厚现金奖励&#xff1f; 机会来啦&#xff01; 2016年9月&#xff0c;飞桨框架正式开源&#xff0c;其兼备易用性、高效性、灵活性和可扩展性等特点。如今&#xff0c;百度飞桨在…

软件工程:说透软件5种常见的部署策略

hi&#xff0c;我是熵减&#xff0c;见字如面。 在软件工程中&#xff0c;最终的价值交付&#xff0c;都是要通过软件的部署上线来完成的。 那如何将新的或改进的软件功能交付给用户&#xff0c;同时还要确保高质量、稳定性和用户体验&#xff0c;选择适当的部署策略变得至关重…

数字中国,开鸿见日

讲个小故事&#xff0c;《晋书乐广传》记载&#xff0c;西晋名士乐广&#xff0c;请大文学家潘岳替自己写一篇文章。潘岳让乐广把意思完完整整告诉他&#xff0c;再由他来动笔&#xff0c;最终写成了名扬当时的《呈太尉辞河南尹表》。时人看过这篇文章&#xff0c;评价乐广是“…

WPF开发txt阅读器5:书籍管理系统,文件夹对话框

文章目录 书柜类文件夹对话框验证 txt阅读器系列&#xff1a; 需求分析和文件读写目录提取类&#x1f48e;列表控件与目录字体控件绑定 书柜类 任何小说阅读器&#xff0c;都免不了要有一个书架功能&#xff0c;而所谓书架&#xff0c;其实就是一个文件夹&#xff0c;通过对…

postgre查询今天,昨天的数据

昨天数据 SELECT* FROMsys_device WHERE age(current_date,to_timestamp(substring(update_time FROM 1 FOR 10),yyyy-MM-dd)) 1 days; 今天数据 SELECT* FROMsys_device WHERE to_char(update_time:: DATE, yyyy-MM-DD hh 24: mi :s) to_char(CURRENT_DATE:: DATE, …

nodejs+vue+mysql汽车新闻资讯网站jtw43

本汽车资讯网站有管理员和用户。管理员功能有个人中心&#xff0c;用户管理&#xff0c;汽车品牌管理&#xff0c;价格分类管理&#xff0c;经销商管理&#xff0c;汽车信息管理&#xff0c;留言板管理&#xff0c;系统管理等。用户可以查看各种汽车信息&#xff0c;还可以进行…

无人机上仅使用CPU实时运行Yolov5(OpenVINO实现)(上篇)

Intel CPU在运行视觉导航等算法时实时性要优于Nvidia等平台&#xff0c;如Jetson Tx2&#xff0c;NX。而Nvidia平台在运行深度学习算法方面具有很大优势&#xff0c;两种平台各有利弊。但是&#xff0c;Intel OpenVINO的推出允许NUC平台实时运行深度学习模型&#xff0c;如目前…

Zookeeper 3.5.7

文章目录 1、Zookeeper 入门1.1 概述1.2 特点1.3 数据结构1.4 应用场景 2、本地安装2.1 本地模式安装2.2 配置参数解读 3、集群操作3.1 集群操作3.1.1 集群安装3.1.2 选举机制&#xff08;面试重点&#xff09;3.1.3 集群启停脚本 3.2 客户端命令行操作3.2.1 命令行语法3.2.2 z…

你没见过的黑科技,真会玩

很久之前分享过谷歌浏览器那些有趣的隐藏功能 &#xff0c;今天再分享几个浏览器黑科技。 第一个是这个项目https://github.com/zserge/awfice &#xff0c;利用data URI浏览器也可以当编辑器&#xff0c;输入data:text/html,<body contenteditable styleline-height:1.5;fo…

电力电子助力小米快充生态,磁材创新驱动未来无线充电

6月14日&#xff0c;第二届非晶合金材料在电力电子产业发展中的机遇与挑战研讨会于上海开幕。小米手机部工程师史学伟作为代表出席本次会议&#xff0c;并作《小米无线快充生态纳米晶合金材料应用及材料发展建议》主题报告&#xff0c;分享小米无线充电技术与产品应用&#xff…

【WinForm】C#实现商场收银软件,从面向过程到面向对象,设计模式的应用

文章目录 前言一、收银系统版本11、运行效果2、界面设计3、代码 二、收银系统版本21、运行效果2、界面设计3、代码&#xff1a; 三、收银系统版本31、运行效果2、界面设计3、代码 四、收银系统版本41、运行效果2、界面设计3、代码 总结面向对象23中设计模式总结设计模式关系图 …

C++类的成员变量和成员函数

类的成员变量和普通变量一样&#xff0c;也有数据类型和名称&#xff0c;占用固定长度的内存。但是在定义类的时候不能对成员变量赋值&#xff1b; 类的成员函数也和普通函数一样&#xff0c;都有返回值和参数列表&#xff0c;它与一般函数的区别是&#xff0c;成员函数是一个…

OpenAI深夜放毒:发布GPT-4新模型,GPT-3.5支持16K上下文,并且价格降低75%

一觉起来&#xff0c;发现OpenAI Twitter更新了&#xff0c;而且更新力度很大&#xff0c;这真是深夜放毒。 下面我们看下OpenAI本次的重大更新都有哪些&#xff1f; 函数调用能力 在Chat Completions API中引入了新的功能调用能力。gpt-4-0613和gpt-3.5-turbo-0613版本已进行了…

原创 | 一文读懂足球比赛中的随机博弈

作者&#xff1a;陈之炎本文约1500字&#xff0c;建议阅读5分钟 本文介绍了如何在足球比赛中读懂随机博弈。 据悉世界足坛的传奇球星梅西将于2023年6月中率领阿根廷国家足球队来到中国北京&#xff0c;参加一场备受瞩目的比赛。这场比赛成为了足球迷们心心念念的盛事&#xff0…

熬夜三晚之深度解析DuckDB MetaPipeline

深度解析DuckDB MetaPipeline 深度解析DuckDB MetaPipeline 1.导语 2.基础理论 3.HashJoin深度解析 3.1 RESULT_COLLECTOR 3.2 PROJECTION 3.3 HASH_JOIN 4.Ready 4.1 翻转 4.2 MetaPipeline与pipeline 5.汇总 1.导语 DuckDB 是…