国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Spring Boot 優(yōu)雅集成 Spring Security 5.7(安全框架)與 JWT(雙令牌機(jī)制)

這篇具有很好參考價值的文章主要介紹了Spring Boot 優(yōu)雅集成 Spring Security 5.7(安全框架)與 JWT(雙令牌機(jī)制)。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報違法"按鈕提交疑問。

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

本章節(jié)將介紹 Spring Boot 集成 Spring Security 5.7(安全框架)。

?? Spring Boot 2.x 實踐案例(代碼倉庫)

介紹

Spring Security 是一個能夠為基于 Spring 的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪問控制解決方案的安全框架。

它提供了一組可以在 Spring 應(yīng)用上下文中配置的 Bean,充分利用了 Spring IOC(控制反轉(zhuǎn)),DI(依賴注入)和 AOP(面向切面編程)功能,為應(yīng)用系統(tǒng)提供聲明式的安全訪問控制功能,減少了為企業(yè)系統(tǒng)安全控制編寫大量重復(fù)代碼的工作。

認(rèn)證和授權(quán)作為 Spring Security 安全框架的核心功能:

認(rèn)證(Authentication):驗證當(dāng)前訪問系統(tǒng)用戶是否是本系統(tǒng)用戶,并且要確認(rèn)具體是哪個用戶。

springboot集成security,后端筆記,spring boot,安全,java

授權(quán)(Authorization):經(jīng)過認(rèn)證后判斷當(dāng)前用戶是否具有權(quán)限進(jìn)行某個操作。

springboot集成security,后端筆記,spring boot,安全,java

快速開始

引入依賴

<!-- 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>

配置文件

# 開發(fā)環(huán)境配置

server:
  # 服務(wù)端口
  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

測試登錄

啟動項目后,嘗試訪問某個接口,會自動跳轉(zhuǎn)到 Spring Security 默認(rèn)登錄頁面。

springboot集成security,后端筆記,spring boot,安全,java

springboot集成security,后端筆記,spring boot,安全,java

默認(rèn)用戶名:user

默認(rèn)密碼:啟動項目時會隨機(jī)生成密碼并輸出在控制臺中:

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

springboot集成security,后端筆記,spring boot,安全,java

認(rèn)證

基于數(shù)據(jù)庫加載用戶

Spring Security 默認(rèn)從內(nèi)存加載用戶,需要實現(xiàn)從數(shù)據(jù)庫加載并校驗用戶。

具體步驟

1)創(chuàng)建 UserServiceImpl

2)實現(xiàn) UserDetailsService 接口

3)重寫 loadUserByUsername 方法

4)根據(jù)用戶名校驗用戶并查詢用戶相關(guān)權(quán)限信息(授權(quán))

5)將數(shù)據(jù)封裝成 UserDetails(創(chuàng)建類并實現(xiàn)該接口) 并返回

核心代碼

LoginUser

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

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

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

    /**
     * 權(quán)限集合
     */
    @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 注解,否則會出現(xiàn)序列化失敗問題

UserServiceImpl

@Service
public class UserServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查詢用戶信息
        UserDO user = getUserByUsername(username);
        // TODO 查詢用戶權(quán)限信息
        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("用戶不存在"));
    }
}

注意:如需測試,需要往用戶表中寫入數(shù)據(jù),并且如果用戶密碼想要明文存儲,需要在密碼前加 {noop}

密碼加密存儲

實際項目中,密碼不會以明文形式存儲在數(shù)據(jù)庫中,而 Spring Security 密碼校驗器要求數(shù)據(jù)庫中密碼格式為:{id}password,而默認(rèn)使用 NoOpPasswordEncoder 加密器,此方式不會對密碼進(jìn)行加密處理,所以不推薦這種形式。

本項目將使用 Spring Security 提供的 BCryptPasswordEncoder 來進(jìn)行密碼校驗。

具體步驟

1)創(chuàng)建 Spring Security Bean 配置類(避免循環(huán)依賴問題)

2)繼承 WebSecurityConfigurerAdapter(舊用法)

3)將 BCryptPasswordEncoder 對象注入 Spring 容器中

注意:Spring Security 5.7.x 版本配置方式與以往有所不同,WebSecurityConfigurerAdapter 在 Spring Security 5.7 版本中已被標(biāo)記 @Deprecated,未來這個類將被移除,本教程將使用繼承 WebSecurityConfigurerAdapter 方式來實現(xiàn) Spring Security 配置,但在實際代碼中配置采用最新版本方式!

Spring Security 版本配置區(qū)別如下:

1)Spring Boot 2.7.0 版本之前,需要寫個配置類繼承 WebSecurityConfigurerAdapter,然后重寫 Adapter 中方法進(jìn)行配置;

2)Spring Boot 2.7.0 版本之后無需再繼承 WebSecurityConfigurerAdapter,只需直接聲明配置類,再配置一個生成 SecurityFilterChainBean 方法,把原來 HttpSecurity 配置移動到該方法中即可。

用的挺順手的 Spring Security 配置類,居然就要被官方棄用了!

核心代碼
@Configuration
public class CommonSecurityConfiguration {

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

注意:同一密碼每次加密后生成密文互不相同,因此需使用 matches() 方法來進(jìn)行比較。

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

}

springboot集成security,后端筆記,spring boot,安全,java

登錄接口

具體步驟

1)創(chuàng)建 Spring Security 配置類

2)生成 SecurityFilterChain Bean 方法

3)放行登錄接口

4)注入 AuthenticationManager 認(rèn)證管理器

5)用戶認(rèn)證

6)生成JWT令牌并返回(雙令牌機(jī)制)

7)訪問令牌(AccessToken)存入 Redis 緩存

注意:Spring Security 5.7.x 版本配置方式與以往有所不同,WebSecurityConfigurerAdapter 在 Spring Security 5.7 版本中已被標(biāo)記 @Deprecated,未來這個類將被移除,所以本教程將采用最新版本配置方式!

Spring Security 版本配置區(qū)別如下:

1)Spring Boot 2.7.0 版本之前,需要寫個配置類繼承 WebSecurityConfigurerAdapter,然后重寫 Adapter 中方法進(jìn)行配置;

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()
              // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證
              .anyRequest()
              .authenticated()
              .and()
              // CSRF禁用
              .csrf().disable()
              // 禁用HTTP響應(yīng)標(biāo)頭
              .headers().cacheControl().disable()
              .and()
              // 基于JWT令牌,無需Session
              .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS));
      return httpSecurity.build();
  }
}

2)注入 AuthenticationManager 認(rèn)證管理器

由于在登錄接口中,需通過 AuthenticationManager 接口中的 authenticate 方法來進(jìn)行用戶認(rèn)證,所以需要在 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)用戶認(rèn)證

調(diào)用 AuthenticationManager 接口中的 authenticate 方法來進(jìn)行用戶認(rèn)證,該方法需傳入 Authentication ,由于 Authentication 是接口,因此需要傳入它的實現(xiàn)類。

由于登錄方式采用賬號密碼形式,所以需使用 UsernamePasswordAuthenticationToken 實現(xiàn)類,此類需傳入用戶名(principal)和密碼(credentials)。

認(rèn)證成功時,Spring Security 將返回 Authentication ,內(nèi)容如下:

springboot集成security,后端筆記,spring boot,安全,java

注意:Authentication 為 NULL 時,說明認(rèn)證沒通過,要么沒查詢到這個用戶,要么密碼比對不通過。

此時還需生成 JWT 令牌,將其放入響應(yīng)中返回,為了能夠?qū)崿F(xiàn)雙令牌機(jī)制需將訪問令牌存入 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);
    }
}

認(rèn)證過濾器

具體步驟

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());
            }
            // 權(quán)限信息
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}

注意:該過濾器實現(xiàn)接口并不是之前的Filter,而是去繼承 OncePerRequestFilter(過濾器抽象類),通常被用于繼承實現(xiàn)并在每次請求時只執(zhí)行一次過濾)。

在配置文件中,將過濾器加到 UsernamePasswordAuthenticationFilter 前面:

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 過濾請求
                .authorizeRequests()
                // 靜態(tài)資源放行
                .antMatchers(STATIC_RESOURCE_WHITE_LIST).permitAll()
                // 接口放行
                .antMatchers(JwtUtil.getWhiteList()).permitAll()
                // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證
                .anyRequest()
                .authenticated()
                .and()
                // CSRF禁用
                .csrf().disable()
                // 禁用HTTP響應(yīng)標(biāo)頭
                .headers().cacheControl().disable()
                .and()
                // 基于JWT令牌,無需Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 攔截器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }
}

退出登錄

JWT最大優(yōu)勢在于它是無狀態(tài),自身包含了認(rèn)證鑒權(quán)所需要的所有信息,服務(wù)器端無需對其存儲,從而給服務(wù)器減少了存儲開銷。

但是無狀態(tài)引出的問題也是可想而知的,它無法作廢未過期的JWT。舉例說明注銷場景下,就傳統(tǒng)的cookie/session認(rèn)證機(jī)制,只需要把存在服務(wù)器端的session刪掉就OK了。

但是JWT呢,它是不存在服務(wù)器端的啊,好的那我刪存在客戶端的JWT行了吧。額,社會本就復(fù)雜別再欺騙自己了好么,被你在客戶端刪掉的JWT還是可以通過服務(wù)器端認(rèn)證的。

使用JWT要非常明確一點(diǎn):JWT失效唯一途徑就是等待時間過期。

本教程借助黑名單方案實現(xiàn)JWT失效:

退出登錄時,將訪問令牌放入 Redis 緩存中,并且設(shè)置過期時間為訪問令牌過期時間;請求資源時判斷該令牌是否在 Redis 中,如果存在則拒絕訪問。

具體步驟

1)全局過濾器中需要判斷黑名單是否存在當(dāng)前訪問令牌

2)解析請求頭中令牌(JTIEXPIRES_AT

3)將JTI字段作為鍵存放到 Redis 緩存中,并設(shè)置訪問令牌過期時間

4)清除認(rèn)證信息

5)配置退出登錄接口與處理器

實戰(zhàn)!退出登錄時如何借助外力使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()
                // 靜態(tài)資源放行
                .antMatchers(STATIC_RESOURCE_WHITE_LIST).permitAll()
                // 接口放行
                .antMatchers(JwtUtil.getWhiteList()).permitAll()
                // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證
                .anyRequest()
                .authenticated()
                .and()
                // CSRF禁用
                .csrf().disable()
                // 禁用HTTP響應(yīng)標(biāo)頭
                .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();
    }
}

授權(quán)

在 Spring Security 中,會使用 FilterSecurityInterceptor(默認(rèn)) 來進(jìn)行權(quán)限校驗。

FilterSecurityInterceptor 中會從 SecurityContextHolder 獲取其中的 AuthenticationAuthentication 包含權(quán)限信息,用來判斷當(dāng)前用戶是否擁有訪問當(dāng)前資源所需的權(quán)限。

具體步驟

1)開啟權(quán)限注解

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

開啟之后,在需要權(quán)限才能訪問的接口上打上 @PreAuthorize 注解即可。

2)查詢用戶權(quán)限信息(見核心代碼)

3)封裝權(quán)限信息

重寫 loadUserByUsername 方法時,查詢出用戶后,還需將用戶對應(yīng)的權(quán)限信息,封裝到之前定義的 UserDetails 的實現(xiàn)類 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;
  
    /**
     * 權(quán)限集合
     */
    @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());
        // 查詢用戶權(quán)限信息
        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>

權(quán)限控制

基于方法注解

Spring Security 默認(rèn)是關(guān)閉方法注解,開啟它只需要通過引入 @EnableGlobalMethodSecurity 注解即可:

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

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

1)prePostEnabled:基于表達(dá)式(Spring EL)注解:

  • @PreAuthorize:進(jìn)入方法之前驗證授權(quán):

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

    表示方法執(zhí)行之前,判斷方法參數(shù)值是否等于 principal 中保存的參數(shù)值;或者當(dāng)前用戶是否具有 ROLE_ADMIN 權(quán)限,兩者符合其中一種即可訪問該方法,內(nèi)置如下方法:

    • hasAuthority:只能傳入一個權(quán)限,只有用戶有這個權(quán)限才可以訪問資源;

    • hasAnyAuthority:可以傳入多個權(quán)限,只有用戶有其中任意一個權(quán)限都可以訪問對應(yīng)資源;

    • hasRole:要求有對應(yīng)角色才可以訪問,但是它內(nèi)部會把傳入的參數(shù)拼接上 ROLE_ 后再去比較:

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

      注意:用戶有 system:dept:list 權(quán)限是無法訪問的,得有 ROLE_system:dept:list 權(quán)限才可以。

    • hasAnyRole:有任意角色即可訪問。

  • @PostAuthorize:檢查授權(quán)方法之后才被執(zhí)行并且可以影響執(zhí)行方法的返回值:

    @PostAuthorize("returnObject.username == authentication.principal.nickName")
    public CustomUser loadUserDetail(String username) {
        return userRoleRepository.loadUserByUserName(username);
    }
    
  • @PostFilter:在方法執(zhí)行之后執(zhí)行,而且這里可以調(diào)用方法的返回值,然后對返回值進(jìn)行過濾或處理或修改并返回。

  • @PreFilter:在方法執(zhí)行之前執(zhí)行,而且這里可以調(diào)用方法的參數(shù),然后對參數(shù)值進(jìn)行過濾或處理或修改。

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 表達(dá)式!

3)jsr250Enabled:開啟對JSR250注解:

  • @DenyAll:拒絕所有權(quán)限

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

  • @PermitAll:接受所有權(quán)限

基于配置文件
方法名稱 方法作用
permitAll() 表示所匹配的URL任何人都允許訪問
anonymous() 表示可以匿名訪問匹配的URL。和permitAll()效果類似,只是設(shè)置為anonymous()的url會執(zhí)行filterChain中的filter
denyAll() 表示所匹配的URL都不允許被訪問。
authenticated() 表示所匹配的URL都需要被認(rèn)證才能訪問
rememberMe() 允許通過remember-me登錄的用戶訪問
access() SpringEl表達(dá)式結(jié)果為true時可以訪問
fullyAuthenticated() 用戶完全認(rèn)證可以訪問(非remember-me下自動登錄)
hasRole() 如果有參數(shù),參數(shù)表示角色,則其角色可以訪問
hasAnyRole() 如果有參數(shù),參數(shù)表示角色,則其中任何一個角色可以訪問
hasAuthority() 如果有參數(shù),參數(shù)表示權(quán)限,則其權(quán)限可以訪問
hasAnyAuthority() 如果有參數(shù),參數(shù)表示權(quán)限,則其中任何一個權(quán)限可以訪問
hasIpAddress() 如果有參數(shù),參數(shù)表示IP地址,如果用戶IP和參數(shù)匹配,則可以訪問

自定義異常處理

在 Spring Security 中,認(rèn)證或者授權(quán)的過程中出現(xiàn)異常會被 ExceptionTranslationFilter 捕獲,在 ExceptionTranslationFilter 中會去判斷是認(rèn)證失敗還是授權(quán)失敗出現(xiàn)的異常。

1)自定義認(rèn)證失敗異常

@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)自定義授權(quán)失敗異常

@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()
                // 靜態(tài)資源放行
                .antMatchers(STATIC_RESOURCE_WHITE_LIST).permitAll()
                // 接口放行
                .antMatchers(JwtUtil.getWhiteList()).permitAll()
                // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證
                .anyRequest()
                .authenticated()
                .and()
                // CSRF禁用
                .csrf().disable()
                // 禁用HTTP響應(yīng)標(biāo)頭
                .headers().cacheControl().disable()
                .and()
                // 基于JWT令牌,無需Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 認(rèn)證與授權(quán)失敗處理類
                .exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint)
                .accessDeniedHandler(restAccessDeniedHandler)
                .and()
                // 攔截器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }
}

跨域

瀏覽器出于安全考慮,使用 XMLHttpRequest 對象發(fā)起 HTTP 請求時必須遵守同源策略,否則就是跨域的 HTTP 請求,默認(rèn)情
況下是被禁止的,同源策略要求源相同才能正常進(jìn)行通信,即協(xié)議、域名、端口號都完全一致。

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 跨域配置文章來源地址http://www.zghlxwxcb.cn/news/detail-650339.html

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

到了這里,關(guān)于Spring Boot 優(yōu)雅集成 Spring Security 5.7(安全框架)與 JWT(雙令牌機(jī)制)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點(diǎn)擊違法舉報進(jìn)行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • SpringBoot2.3集成Spring Security(二) JWT認(rèn)證

    SpringBoot2.3集成Spring Security(二) JWT認(rèn)證

    緊接上文,我們已經(jīng)完成了 SpringBoot中集成Spring Security,并且用戶名帳號和密碼都是從數(shù)據(jù)庫中獲取。但是這種方式還是不能滿足現(xiàn)在的開發(fā)需求。 使用JWT的好處: 無狀態(tài)認(rèn)證:JWT本身包含了認(rèn)證信息和聲明,服務(wù)器不需要在會話中保存任何狀態(tài)。這樣使得應(yīng)用程序可以更加

    2024年02月11日
    瀏覽(21)
  • 【Spring Security系列】Spring Security整合JWT:構(gòu)建安全的Web應(yīng)用

    【Spring Security系列】Spring Security整合JWT:構(gòu)建安全的Web應(yīng)用

    在企業(yè)級開發(fā)或者我們自己的課程設(shè)計中,確保用戶數(shù)據(jù)的安全性和訪問控制非常重要。而Spring Security和JWT是都兩個強(qiáng)大的工具,它倆結(jié)合可以幫助我們實現(xiàn)這一目標(biāo)。 Spring Security提供了全面的安全功能,而JWT則是一種用于身份驗證的令牌機(jī)制。 前面兩個章節(jié)介紹過了Spri

    2024年04月23日
    瀏覽(24)
  • 在Spring Boot框架中集成 Spring Security

    技術(shù)介紹 SpringSecurity的核心功能: SpringSecurity特點(diǎn): 具體實現(xiàn) 1、集成依賴 2、修改spring security 實現(xiàn)service.impl.UserDetailsServiceImpl類 代碼1具體解釋 代碼2具體解釋 實現(xiàn)config.SecurityConfig類 代碼具體解釋 總結(jié) Spring Security是一個基于Spring框架的安全性框架,它提供了一系列的安全性

    2024年02月14日
    瀏覽(30)
  • Spring boot框架 JWT實現(xiàn)用戶賬戶密碼登錄驗證

    Spring boot框架 JWT實現(xiàn)用戶賬戶密碼登錄驗證

    目錄 1、JWT定義 1、1 JWT工作流程 1、2 JWT優(yōu)點(diǎn) 2、添加依賴項到pom.xml? 3、創(chuàng)建用戶實體類 ?4、實現(xiàn)認(rèn)證服務(wù) 5、登錄請求處理 6、生成JWT JWT(JSON Web Token)是一種用于在網(wǎng)絡(luò)應(yīng)用間傳遞信息的安全傳輸方式。它是一種緊湊且自包含的方式,通過使用數(shù)字簽名來驗證數(shù)據(jù)的完整性

    2024年02月07日
    瀏覽(21)
  • Spring Boot 集成 EasyExcel 3.x 優(yōu)雅實現(xiàn)Excel導(dǎo)入導(dǎo)出

    Spring Boot 集成 EasyExcel 3.x 優(yōu)雅實現(xiàn)Excel導(dǎo)入導(dǎo)出

    本章節(jié)將介紹 Spring Boot 集成 EasyExcel(優(yōu)雅實現(xiàn)Excel導(dǎo)入導(dǎo)出)。 ?? Spring Boot 2.x 實踐案例(代碼倉庫) EasyExcel 是一個基于 Java 的、快速、簡潔、解決大文件內(nèi)存溢出的 Excel 處理工具。它能讓你在不用考慮性能、內(nèi)存的等因素的情況下,快速完成 Excel 的讀、寫等功能。 Ea

    2024年02月03日
    瀏覽(52)
  • Spring Boot + JWT = 安全無憂的RESTful API

    在構(gòu)建現(xiàn)代Web應(yīng)用程序時,安全性是一個不可或缺的要素。JSON Web Token(JWT)提供了一種簡潔的方式來保護(hù)我們的RESTful接口。在本篇博客中,我們將一步步探索如何在Spring Boot應(yīng)用中整合JWT,確保你的API安全、高效且易于管理。 JWT(JSON Web Token)是一個開放標(biāo)準(zhǔn)(RFC 7519),它

    2024年02月02日
    瀏覽(20)
  • Spring Security(安全框架)

    (1)Spring Security 是一個高度自定義的 安全框架 。利用Spring IoC/DI和AOP功能,為系統(tǒng)提供了聲明式安全訪問控制功能,減少了為系統(tǒng)安全而編寫大量重復(fù)代碼的工作。 (2)認(rèn)證(Authentication): 應(yīng)用程序確認(rèn)用戶身份的過程,常見認(rèn)證:登錄。 (3)身份(principal)/主體(

    2023年04月13日
    瀏覽(19)
  • Spring Boot安全管理—Spring Security基本配置

    Spring Boot安全管理—Spring Security基本配置

    1.1 創(chuàng)建項目,添加依賴 創(chuàng)建一個Spring Boot Web 項目,然后添加spring-boot-starter-security依賴。 1.2 添加hello接口 在項目中添加一個簡單的/hello接口,內(nèi)容如下: 1.3 啟動項目測試 訪問/hello接口會自動跳轉(zhuǎn)到登錄頁面,這個頁面有Spring Security提供的。 默認(rèn)的用戶名是user,默認(rèn)的登

    2024年02月08日
    瀏覽(21)
  • 【Spring Security】安全框架學(xué)習(xí)(八)

    3.0 權(quán)限系統(tǒng)的作用 例如一個學(xué)校圖書館的管理系統(tǒng),如果是普通學(xué)生登錄就能看到借書還書相關(guān)的功能,不可能讓他看到并且去使用添加書籍信息,刪除書籍信息等功能。但是如果是一個圖書館管理員的賬號登錄了,應(yīng)該就能看到并使用添加書籍信息,刪除書籍信息等功能

    2024年02月09日
    瀏覽(21)
  • 【Spring Security】安全框架學(xué)習(xí)(十二)

    【Spring Security】安全框架學(xué)習(xí)(十二)

    6.0 其他權(quán)限校驗方法 我們前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法進(jìn)行校驗。Spring Security還為我們提供了其它方法. 例如:hasAnyAuthority,hasRole,hasAnyRole,等。 這里我們先不急著去介紹這些方法,我們先去理解hasAuthority的原理,然后再去學(xué)習(xí)其他方法就

    2024年02月07日
    瀏覽(23)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包