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

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

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):验证当时拜访体系用户是否是本体系用户,而且要确认具体是哪个用户。

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

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

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

快速开始

引进依靠

<!-- 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 默许登录页面。

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

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

默许用户名:user

默许暗码:启动项目时会随机生成暗码并输出在操控台中:

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

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

认证

根据数据库加载用户

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);
    }
}

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

登录接口

具体步骤

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 ,内容如下:

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

留意: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();
    }
}