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)具體是哪個用戶。
授權(quán)(Authorization):經(jīng)過認(rèn)證后判斷當(dāng)前用戶是否具有權(quán)限進(jìn)行某個操作。
快速開始
引入依賴
<!-- 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)登錄頁面。
默認(rèn)用戶名:user
默認(rèn)密碼:啟動項目時會隨機(jī)生成密碼并輸出在控制臺中:
Using generated security password: 0b7bb972-ab4c-461c-ab19-7824d23d9b87
認(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);
}
}
登錄接口
具體步驟
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)容如下:
注意: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)解析請求頭中令牌(JTI
與 EXPIRES_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 獲取其中的 Authentication,Authentication 包含權(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 跨域配置文章來源:http://www.zghlxwxcb.cn/news/detail-650339.html
@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)!