SpringSecurity進(jìn)行認(rèn)證與授權(quán)的示例代碼
一、SpringSecurity簡(jiǎn)介
Spring Security 是 Spring 家族中的一個(gè)安全管理框架。相比與另外一個(gè)安全框架Shiro,它提供了更豐富的功能,社區(qū)資源也比Shiro豐富。
一般來(lái)說(shuō)中大型的項(xiàng)目都是使用SpringSecurity 來(lái)做安全框架。小項(xiàng)目有Shiro的比較多,因?yàn)橄啾扰cSpringSecurity,Shiro的上手更加的簡(jiǎn)單。
一般Web應(yīng)用的需要進(jìn)行認(rèn)證和授權(quán)。
- 認(rèn)證:驗(yàn)證當(dāng)前訪問(wèn)系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認(rèn)具體是哪個(gè)用戶
- 授權(quán):經(jīng)過(guò)認(rèn)證后判斷當(dāng)前用戶是否有權(quán)限進(jìn)行某個(gè)操作
而認(rèn)證和授權(quán)也是SpringSecurity作為安全框架的核心功能。
1.1 入門Demo
依賴如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>引入依賴后我們?cè)趪L試去訪問(wèn)之前的接口就會(huì)自動(dòng)跳轉(zhuǎn)到一個(gè)SpringSecurity的默認(rèn)登陸頁(yè)面,默認(rèn)用戶名是user,密碼會(huì)輸出在控制臺(tái)。

必須登陸之后才能對(duì)接口進(jìn)行訪問(wèn)。

訪問(wèn) localhost:8080/logout 這個(gè)鏈接可以 對(duì)其進(jìn)行退出操作。

Ps:
以上過(guò)程了解即可,因?yàn)槲覀儗?shí)際Web項(xiàng)目中,一般采用我們自定義的登錄驗(yàn)證授權(quán)方案,不會(huì)采取SpringSecurity框架提供的默認(rèn)方案。
二、認(rèn)證
登錄校驗(yàn)流程:

為了實(shí)現(xiàn)以上這種過(guò)程,我們需要先對(duì)SpringSecurity默認(rèn)的流程進(jìn)行了解,才可以對(duì)其進(jìn)行修改,實(shí)現(xiàn)我們自定義的方案。
2.1 SpringSecurity完整流程
SpringSecurity的原理其實(shí)就是一個(gè)過(guò)濾器鏈,內(nèi)部包含了提供各種功能的過(guò)濾器。這里我們可以看看入門案例中的過(guò)濾器:

圖中只展示了核心過(guò)濾器,其它的非核心過(guò)濾器并沒(méi)有在圖中展示:
- UsernamePasswordAuthenticationFilter:負(fù)責(zé)處理我們?cè)诘顷戫?yè)面填寫了用戶名密碼后的登陸請(qǐng)求。入門案例的認(rèn)證工作主要有它負(fù)責(zé)。
- ExceptionTranslationFilter:處理過(guò)濾器鏈中拋出的任何AccessDeniedException和AuthenticationException 。
- FilterSecurityInterceptor:負(fù)責(zé)權(quán)限校驗(yàn)的過(guò)濾器。我們可以通過(guò)Debug查看當(dāng)前系統(tǒng)中SpringSecurity過(guò)濾器鏈中有哪些過(guò)濾器及它們的順序。
如果想查看所有的過(guò)濾器,可以通過(guò)獲取Spring容器,Debug方式來(lái)查看:

2.2 認(rèn)證流程詳解
箭頭代表該方法屬于這個(gè)實(shí)現(xiàn)類的。

概念速查:
- Authentication接口: 它的實(shí)現(xiàn)類,表示當(dāng)前訪問(wèn)系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
- AuthenticationManager接口:定義了認(rèn)證Authentication的方法
- UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個(gè)根據(jù)用戶名查詢用戶信息的方法。
- UserDetails接口:提供核心用戶信息。通過(guò)UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對(duì)象返回。然后將這些信息封裝到Authentication對(duì)象中。
2.3 自定義認(rèn)證實(shí)現(xiàn)
登錄
①自定義登錄接口
調(diào)用ProviderManager的方法進(jìn)行認(rèn)證 如果認(rèn)證通過(guò)生成jwt 把用戶信息存入redis中
②自定義UserDetailsService
在這個(gè)實(shí)現(xiàn)類中去查詢數(shù)據(jù)庫(kù)
校驗(yàn)
①定義Jwt 認(rèn)證過(guò)濾器
獲取token 解析token獲取其中的userid
從redis中獲取用戶信息
存入SecurityContextHolder

這里為什么要存入 SecurityContextHolder中呢?
我們自定義的JWT過(guò)濾器的時(shí)候,肯定是需要將這個(gè)JWT過(guò)濾器放在UsernamePasswordAuthenticationFilter前的,這時(shí)我們將從redis獲取的用戶信息存入SecurityContextHolder才行,否則后續(xù)過(guò)濾器在進(jìn)行校驗(yàn)的時(shí)候,可能會(huì)因?yàn)镾ecurityContextHolder中沒(méi)有對(duì)應(yīng)的值而判斷當(dāng)前訪問(wèn)用戶驗(yàn)證不通過(guò)。

2.3.1 數(shù)據(jù)庫(kù)校驗(yàn)用戶
定義Mapper接口
package com.example.springsecurity_demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springsecurity_demo.domain.User;
public interface UserMapper extends BaseMapper<User> {
}定義User實(shí)體類
package com.example.springsecurity_demo.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class User implements Serializable {
private static final long serialVersionUID = -40356785423868312L;
/**
* 主鍵
*/
@TableId
private Long id;
/**
* 用戶名
*/
private String userName;
/**
* 昵稱
*/
private String nickName;
/**
* 密碼
*/
private String password;
/**
* 賬號(hào)狀態(tài)(0正常 1停用)
*/
private String status;
/**
* 郵箱
*/
private String email;
/**
* 手機(jī)號(hào)
*/
private String phonenumber;
/**
* 用戶性別(0男,1女,2未知)
*/
private String sex;
/**
* 頭像
*/
private String avatar;
/**
* 用戶類型(0管理員,1普通用戶)
*/
private String userType;
/**
* 創(chuàng)建人的用戶id
*/
private Long createBy;
/**
* 創(chuàng)建時(shí)間
*/
private Date createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新時(shí)間
*/
private Date updateTime;
/**
* 刪除標(biāo)志(0代表未刪除,1代表已刪除)
*/
private Integer delFlag;
}配置Mapper掃描
package com.example.springsecurity_demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
@MapperScan("com.example.springsecurity_demo.mapper")
public class SpringSecurityDemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringSecurityDemoApplication.class, args);
System.out.println(1);
}
}
核心代碼實(shí)現(xiàn)
package com.example.springsecurity_demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.springsecurity_demo.domain.LoginUser;
import com.example.springsecurity_demo.domain.User;
import com.example.springsecurity_demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查詢用戶信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
// 如果沒(méi)有查詢到用戶就拋出異常
if (Objects.isNull(user)) {
throw new RuntimeException("用戶名或者密碼錯(cuò)誤");
}
//TODO 查詢對(duì)應(yīng)的權(quán)限信息
return new LoginUser(user);
}
}
因?yàn)閁serDetailsService方法的返回值是UserDetails(接口):

所以需要定義一個(gè)類,實(shí)現(xiàn)該接口,把用戶信息封裝在其中。
package com.example.springsecurity_demo.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
注意:如果要測(cè)試,需要往用戶表中寫入用戶數(shù)據(jù),并且如果你想讓用戶的密碼是明文存儲(chǔ),需要在密碼前加{noop}。例如:

這樣登陸的時(shí)候就可以用fox作為用戶名,123作為密碼來(lái)登陸了。
2.3.2 密碼加密存儲(chǔ)
實(shí)際項(xiàng)目中我們不會(huì)把密碼明文存儲(chǔ)在數(shù)據(jù)庫(kù)中。
默認(rèn)使用的PasswordEncoder要求數(shù)據(jù)庫(kù)中的密碼格式為:{id}password 。它會(huì)根據(jù)id去判斷密碼的加密方式。但是我們一般不會(huì)采用這種方式。所以就需要替換PasswordEncoder。
我們一般使用SpringSecurity為我們提供的BCryptPasswordEncoder。
我們只需要使用把BCryptPasswordEncoder對(duì)象注入Spring容器中,SpringSecurity就會(huì)使用該
PasswordEncoder來(lái)進(jìn)行密碼校驗(yàn)。
我們可以定義一個(gè)SpringSecurity的配置類:
低版本配置如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}以下是高版本的SpringSecurity(SpringBoot 3 用以下配置):
package com.example.springsecurity_demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.3.3 登錄接口實(shí)現(xiàn)
接下來(lái)我們需要自定義登錄接口,這里我們需要讓SpringSecurity對(duì)這個(gè)接口放行,讓用戶訪問(wèn)這個(gè)接口的時(shí)候不用登錄也能訪問(wèn)。(畢竟登錄接口如果還需要權(quán)限訪問(wèn),那么就很奇怪了)
在接口中我們通過(guò)AuthenticationManager的authenticate方法來(lái)進(jìn)行用戶認(rèn)證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器中。
認(rèn)證成功的話要生成一個(gè)JWT,放入響應(yīng)中返回,并且為了讓用戶下回請(qǐng)求時(shí)需要通過(guò)JWT識(shí)別出具體的是哪個(gè)用戶,我們需要把用戶信息存入redis,可以把用戶id作為key。
Contorller類如下:
package com.example.springsecurity_demo.controller;
import com.example.springsecurity_demo.domain.ResponseResult;
import com.example.springsecurity_demo.domain.User;
import com.example.springsecurity_demo.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
return loginService.login(user);
}
}Ps:
雖然字段聲明的類型是 LoginService,但實(shí)際上注入的是 LoginServiceImpl。這是因?yàn)?LoginServiceImpl 實(shí)現(xiàn)了 LoginService 接口,因此它被視為 LoginService 的一種類型。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//創(chuàng)建BCryptPasswordEncoder注入容器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//關(guān)閉csrf
.csrf().disable()
//不通過(guò)Session獲取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 對(duì)于登錄接口 允許匿名訪問(wèn)
.antMatchers("/user/login").anonymous()
// .antMatchers("/testCors").hasAuthority("system:dept:list222")
// 除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
.anyRequest().authenticated();
//
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
實(shí)現(xiàn)類如下:
import com.sangeng.domain.LoginUser;
import com.sangeng.domain.ResponseResult;
import com.sangeng.domain.User;
import com.sangeng.service.LoginServcie;
import com.sangeng.utils.JwtUtil;
import com.sangeng.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Service
public class LoginServiceImpl implements LoginServcie {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
//AuthenticationManager authenticate進(jìn)行用戶認(rèn)證
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果認(rèn)證沒(méi)通過(guò),給出對(duì)應(yīng)的提示
if(Objects.isNull(authenticate)){
throw new RuntimeException("登錄失敗");
}
//如果認(rèn)證通過(guò)了,使用userid生成一個(gè)jwt jwt存入ResponseResult返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userid = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userid);
Map<String,String> map = new HashMap<>();
map.put("token",jwt);
//把完整的用戶信息存入redis userid作為key
redisCache.setCacheObject("login:"+userid,loginUser);
return new ResponseResult(200,"登錄成功",map);
}
}
分析:
這里創(chuàng)建UsernamePasswordAuthenticationToken對(duì)象是因?yàn)檎{(diào)用authenticationManager.authenticate方法 需要傳入Authentication,但是Authentication又是一個(gè)接口,所以需要傳入其實(shí)現(xiàn)類。
2.3.4 認(rèn)證過(guò)濾器
我們需要自定義一個(gè)過(guò)濾器,這個(gè)過(guò)濾器會(huì)去獲取請(qǐng)求頭中的token,對(duì)token進(jìn)行解析取出其中的
userid。
使用userid去redis中獲取對(duì)應(yīng)的LoginUser對(duì)象。
然后封裝Authentication對(duì)象存入SecurityContextHolder。
import com.sangeng.domain.LoginUser;
import com.sangeng.utils.JwtUtil;
import com.sangeng.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//獲取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;
}
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//從redis中獲取用戶信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用戶未登錄");
}
//存入SecurityContextHolder
//TODO 獲取權(quán)限信息封裝到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
分析:
這里先設(shè)置放行最后再使用return是因?yàn)?,如果該?qǐng)求無(wú)含token,那么就對(duì)其進(jìn)行放行,讓請(qǐng)求進(jìn)入下一個(gè)攔截器,后續(xù)Security框架還有很多攔截器可以對(duì)其進(jìn)行驗(yàn)證,而使用return是因?yàn)楹罄m(xù)在進(jìn)行參數(shù)返回的時(shí)候,不需要再執(zhí)行以下代碼。
簡(jiǎn)單來(lái)說(shuō):圖中代碼紅線以上部分是參數(shù)請(qǐng)求時(shí)候所執(zhí)行的部分,紅線以下是返回響應(yīng)體時(shí)候所執(zhí)行的部分。

還有個(gè)細(xì)節(jié)是:這里必須調(diào)用三個(gè)參數(shù)的構(gòu)造方法,而不是兩個(gè),這是因?yàn)橹挥姓{(diào)用三個(gè)構(gòu)造方法的時(shí)候,才能保證該主體是認(rèn)證過(guò)的,否則框架檢查時(shí)候還是會(huì)報(bào)錯(cuò):

SecurityConfig如下:
import com.sangeng.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//創(chuàng)建BCryptPasswordEncoder注入容器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//關(guān)閉csrf
.csrf().disable()
//不通過(guò)Session獲取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 對(duì)于登錄接口 允許匿名訪問(wèn)
.antMatchers("/user/login").anonymous()
// .antMatchers("/testCors").hasAuthority("system:dept:list222")
// 除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
.anyRequest().authenticated();
//添加過(guò)濾器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//配置異常處理器
http.exceptionHandling()
//配置認(rèn)證失敗處理器
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
//允許跨域
http.cors();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
2.3.5 退出登錄
我們只需要定義一個(gè)登陸接口,然后獲取SecurityContextHolder中的認(rèn)證信息,刪除redis中對(duì)應(yīng)的數(shù)據(jù)即可。
import com.sangeng.domain.LoginUser;
import com.sangeng.domain.ResponseResult;
import com.sangeng.domain.User;
import com.sangeng.service.LoginServcie;
import com.sangeng.utils.JwtUtil;
import com.sangeng.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Service
public class LoginServiceImpl implements LoginServcie {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult logout() {
//獲取SecurityContextHolder中的用戶id
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userid = loginUser.getUser().getId();
//刪除redis中的值
redisCache.deleteObject("login:"+userid);
return new ResponseResult(200,"注銷成功");
}
}
分析:
這里并不需要?jiǎng)h除SecurityContextHolder中的信息,只需要?jiǎng)h除redis中所存儲(chǔ)的即可,因?yàn)樵谶M(jìn)行認(rèn)證的時(shí)候,需要先在SecurityContextHolder中拿到信息后,再?gòu)膔edis中獲取對(duì)應(yīng)信息。
每個(gè)請(qǐng)求都對(duì)應(yīng)一個(gè)SecurityContextHolder,所以刪除SecurityContextHolder中的信息是無(wú)效的,需要?jiǎng)h除redis中所存儲(chǔ)的信息。
三、授權(quán)
3.1 權(quán)限系統(tǒng)作用
例如一個(gè)學(xué)校圖書館的管理系統(tǒng),如果是普通學(xué)生登錄就能看到借書還書相關(guān)的功能,不可能讓他看到并且去使用添加書籍信息,刪除書籍信息等功能。但是如果是一個(gè)圖書館管理員的賬號(hào)登錄了,應(yīng)該就能看到并使用添加書籍信息,刪除書籍信息等功能。
總結(jié)起來(lái)就是不同的用戶可以使用不同的功能。這就是權(quán)限系統(tǒng)要去實(shí)現(xiàn)的效果。
我們不能只依賴前端去判斷用戶的權(quán)限來(lái)選擇顯示哪些菜單哪些按鈕。因?yàn)槿绻皇沁@樣,如果有人知道了對(duì)應(yīng)功能的接口地址就可以不通過(guò)前端,直接去發(fā)送請(qǐng)求來(lái)實(shí)現(xiàn)相關(guān)功能操作。
所以我們還需要在后臺(tái)進(jìn)行用戶權(quán)限的判斷,判斷當(dāng)前用戶是否有相應(yīng)的權(quán)限,必須具有所需權(quán)限才能進(jìn)行相應(yīng)的操作。
3.2 授權(quán)基本流程
在SpringSecurity中,會(huì)使用默認(rèn)的FilterSecurityInterceptor來(lái)進(jìn)行權(quán)限校驗(yàn)。在
FilterSecurityInterceptor中會(huì)從SecurityContextHolder獲取其中的Authentication,然后獲取其中的
權(quán)限信息。當(dāng)前用戶是否擁有訪問(wèn)當(dāng)前資源所需的權(quán)限。
所以我們?cè)陧?xiàng)目中只需要把當(dāng)前登錄用戶的權(quán)限信息也存入Authentication。
然后設(shè)置我們的資源所需要的權(quán)限即可。
如何將權(quán)限信息存入Authentication呢?
回顧之前的代碼,我們需要在自定義的認(rèn)證過(guò)濾器中,將Authentication存入SecurityContextHolder中,那么Authentication的信息又是從哪里來(lái)呢?
:是從redis中獲取到的。

也就是我們當(dāng)初應(yīng)該往redis中存入權(quán)限信息,而redis中存儲(chǔ)的是loginUser,
loginUser是當(dāng)初我們UserDetail的自定義實(shí)現(xiàn)類中所查詢到的用戶信息(也就是:從數(shù)據(jù)庫(kù)中查詢的)。

而最終這個(gè)loginUser對(duì)象會(huì)返回給這個(gè)authenticate對(duì)象,那么authenticate這個(gè)對(duì)象就會(huì)存入redis中。

以上就是整個(gè)流程,所以我們最終只需要完成兩個(gè)步驟即可:
- 在查詢數(shù)據(jù)庫(kù)的時(shí)候獲取對(duì)應(yīng)的權(quán)限信息。
- 在實(shí)現(xiàn)認(rèn)證過(guò)濾器的時(shí)候,需要獲取當(dāng)前用戶的權(quán)限信息,并存入到SecurityContextHolder中。
也就是需要完善上述圖片中兩個(gè) TODO 標(biāo)識(shí)的代碼片段。
3.3 授權(quán)實(shí)現(xiàn)
3.3.1 限制訪問(wèn)資源所需權(quán)限
SpringSecurity為我們提供了基于注解的權(quán)限控制方案,這也是我們項(xiàng)目中主要采用的方式。我們可以使用注解去指定訪問(wèn)對(duì)應(yīng)的資源所需的權(quán)限。
但是要使用它我們需要先開(kāi)啟相關(guān)配置,即在關(guān)于SpringSecurity的配置類中添加以下代碼:
@EnableGlobalMethodSecurity(prePostEnabled = true)

設(shè)置完之后,就可以使用對(duì)應(yīng)的注解:@PreAuthorize。
@RestController
public class HelloController {
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('test')")
public String hello(){
return "hello";
}
}分析:
這里的test只供測(cè)試,test使用單引號(hào)是因?yàn)橥鈱右呀?jīng)有了雙引號(hào),所以使用單引號(hào)來(lái)標(biāo)識(shí)這是個(gè)字符串,實(shí)際上我們通過(guò)idea的提示(ctrl+p),也可以知道,這個(gè)hasAuthority所需要的參數(shù)也是String類型:

3.3.2 封裝權(quán)限信息
我們前面在寫UserDetailsServiceImpl的時(shí)候說(shuō)過(guò),在查詢出用戶后還要獲取對(duì)應(yīng)的權(quán)限信息,封裝到UserDetails中返回。
我們先直接把權(quán)限信息寫死封裝到UserDetails中進(jìn)行測(cè)試。
我們之前定義了UserDetails的實(shí)現(xiàn)類LoginUser,想要讓其能封裝權(quán)限信息就要對(duì)其進(jìn)行修改。
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
private List<String> permissions;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
//把permissions中String類型的權(quán)限信息封裝成SimpleGrantedAuthority對(duì)象
authorities = new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
// authorities = permissions.stream()
// .map(SimpleGrantedAuthority::new)
// .collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
分析:
這里我們重寫了getAuthorities方法, permission這個(gè)屬性存儲(chǔ)的是權(quán)限信息。
在getAuthorities方法中,由于返回值是一個(gè)Collection類型,所以我們這里選擇List集合。
又因?yàn)榉盒鸵笫荊rantedAuthority的子類,但是其是一個(gè)接口,所以我們通過(guò)查找其實(shí)現(xiàn)類(ctrl+alt+鼠標(biāo)左鍵):

選擇了SimpleGrantedAuthority,因?yàn)榭此钟袀€(gè)simple,再觀察其構(gòu)造方法,發(fā)現(xiàn)只需要傳入一個(gè)字符串即可,于是我們就需要將我們類屬性的permission全部都通過(guò)構(gòu)造方法存入SimpleGrantedAuthority屬性中,然后將其一個(gè)個(gè)遍歷,放入list集合中。

還有一個(gè)小細(xì)節(jié)是這里加了@JSONField(serialize = false) 注解防止redis存儲(chǔ)loginUser時(shí)候序列化出錯(cuò)(報(bào)異常),因?yàn)镾impleGrantedAuthority是Spring中提供的類。
雖然我們這里不讓這個(gè)List集合序列化,但是并不影響后續(xù)操作,因?yàn)樵谌〕鰜?lái)反序列化的時(shí)候,我們自定義的permission屬性是可以被正常序列化的,那個(gè)時(shí)候通過(guò)它就可以讓程序正常運(yùn)行。

3.3.3 從數(shù)據(jù)庫(kù)查詢權(quán)限信息
3.3.3.1 RBAC權(quán)限模型
RBAC權(quán)限模型(Role-Based Access Control)即:基于角色的權(quán)限控制。這是目前最常被開(kāi)發(fā)者使用也是相對(duì)易用、通用權(quán)限模型。

3.3.3.2 代碼實(shí)現(xiàn)
Menu類如下:
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* 菜單表(Menu)實(shí)體類
*
* @author makejava
* @since 2021-11-24 15:30:08
*/
@TableName(value="sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {
private static final long serialVersionUID = -54979041104113736L;
@TableId
private Long id;
/**
* 菜單名
*/
private String menuName;
/**
* 路由地址
*/
private String path;
/**
* 組件路徑
*/
private String component;
/**
* 菜單狀態(tài)(0顯示 1隱藏)
*/
private String visible;
/**
* 菜單狀態(tài)(0正常 1停用)
*/
private String status;
/**
* 權(quán)限標(biāo)識(shí)
*/
private String perms;
/**
* 菜單圖標(biāo)
*/
private String icon;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
/**
* 是否刪除(0未刪除 1已刪除)
*/
private Integer delFlag;
/**
* 備注
*/
private String remark;
}mapper如下:
我們只需要根據(jù)用戶id去查詢到其所對(duì)應(yīng)的權(quán)限信息即可。
所以我們可以先定義個(gè)mapper,其中提供一個(gè)方法可以根據(jù)userid查詢權(quán)限信息。
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sangeng.domain.Menu;
import java.util.List;
public interface MenuMapper extends BaseMapper<Menu> {
List<String> selectPermsByUserId(Long userid);
}
尤其是自定義方法,所以需要?jiǎng)?chuàng)建對(duì)應(yīng)的mapper文件,定義對(duì)應(yīng)的sql語(yǔ)句
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.fox.mapper.MenuMapper">
<select id="selectPermsByUserId" resultType="java.lang.String">
SELECT
DISTINCT m.`perms`
FROM
sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
user_id = #{userid}
AND r.`status` = 0
AND m.`status` = 0
</select>
</mapper>然后我們可以在UserDetailsServiceImpl中去調(diào)用該mapper的方法查詢權(quán)限信息封裝到LoginUser對(duì)象中即可:
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sangeng.domain.LoginUser;
import com.sangeng.domain.User;
import com.sangeng.mapper.MenuMapper;
import com.sangeng.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查詢用戶信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
// 如果沒(méi)有查詢到用戶就拋出異常
if(Objects.isNull(user)){
throw new RuntimeException("用戶名或者密碼錯(cuò)誤");
}
// List<String> list = new ArrayList<>(Arrays.asList("test","admin"));
List<String> list = menuMapper.selectPermsByUserId(user.getId());
//把數(shù)據(jù)封裝成UserDetails返回
return new LoginUser(user,list);
}
}
四、自定義失敗處理
我們還希望在認(rèn)證失敗或者是授權(quán)失敗的情況下也能和我們的接口一樣返回相同結(jié)構(gòu)的json,這樣可以讓前端能對(duì)響應(yīng)進(jìn)行統(tǒng)一的處理。要實(shí)現(xiàn)這個(gè)功能我們需要知道SpringSecurity的異常處理機(jī)制。
在SpringSecurity中,如果我們?cè)谡J(rèn)證或者授權(quán)的過(guò)程中出現(xiàn)了異常會(huì)被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會(huì)去判斷是認(rèn)證失敗還是授權(quán)失敗出現(xiàn)的異常。
如果是認(rèn)證過(guò)程中出現(xiàn)的異常會(huì)被封裝成AuthenticationException然后調(diào)用
AuthenticationEntryPoint對(duì)象的方法去進(jìn)行異常處理。
如果是授權(quán)過(guò)程中出現(xiàn)的異常會(huì)被封裝成AccessDeniedException然后調(diào)用AccessDeniedHandler對(duì)象的方法去進(jìn)行異常處理。
所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和
AccessDeniedHandler然后配置給SpringSecurity即可。
4.1 創(chuàng)建自定義實(shí)現(xiàn)類
也就是說(shuō),我們只需要?jiǎng)?chuàng)建一個(gè)自定義的實(shí)現(xiàn)類然后分別去實(shí)現(xiàn)AccessDeniedHandler接口和AccessDeniedHandler接口即可,代碼如下。
認(rèn)證失敗自定義實(shí)現(xiàn)類如下:
import com.alibaba.fastjson.JSON;
import com.sangeng.domain.ResponseResult;
import com.sangeng.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"用戶認(rèn)證失敗請(qǐng)查詢登錄");
String json = JSON.toJSONString(result);
//處理異常
WebUtils.renderString(response,json);
}
}
授權(quán)失敗自定義實(shí)現(xiàn)類如下:
import com.alibaba.fastjson.JSON;
import com.sangeng.domain.ResponseResult;
import com.sangeng.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(),"您的權(quán)限不足");
String json = JSON.toJSONString(result);
//處理異常
WebUtils.renderString(response,json);
}
}
涉及到的工具類如下:
由于response對(duì)象是較為原生的,所以我們需要進(jìn)行書寫狀態(tài)碼,ContentType等。所以我們需要使用工具類對(duì)其進(jìn)行修改。
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class WebUtils
{
/**
* 將字符串渲染到客戶端
*
* @param response 渲染對(duì)象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}4.2 將實(shí)現(xiàn)類配置給SpringSecurity
注入對(duì)應(yīng)處理器:

然后我們可以使用HttpSecurity對(duì)象的方法去配置:

配合類代碼如下:
import com.sangeng.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//創(chuàng)建BCryptPasswordEncoder注入容器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//關(guān)閉csrf
.csrf().disable()
//不通過(guò)Session獲取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 對(duì)于登錄接口 允許匿名訪問(wèn)
.antMatchers("/user/login").anonymous()
// .antMatchers("/testCors").hasAuthority("system:dept:list222")
// 除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
.anyRequest().authenticated();
//添加過(guò)濾器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//配置異常處理器
http.exceptionHandling()
//配置認(rèn)證失敗處理器
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
//允許跨域
http.cors();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
五、跨域問(wèn)題解決方案
瀏覽器出于安全的考慮,使用 XMLHttpRequest對(duì)象發(fā)起 HTTP請(qǐng)求時(shí)必須遵守同源策略,否則就是跨域的HTTP請(qǐng)求,默認(rèn)情況下是被禁止的。 同源策略要求源相同才能正常進(jìn)行通信,即協(xié)議、域名、端口號(hào)都完全一致。
前后端分離項(xiàng)目,前端項(xiàng)目和后端項(xiàng)目一般都不是同源的,所以肯定會(huì)存在跨域請(qǐng)求的問(wèn)題。
所以我們就要處理一下,讓前端能進(jìn)行跨域請(qǐng)求。
①先對(duì)SpringBoot配置,運(yùn)行跨域請(qǐng)求
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 設(shè)置允許跨域的路徑
registry.addMapping("/**")
// 設(shè)置允許跨域請(qǐng)求的域名
.allowedOriginPatterns("*")
// 是否允許cookie
.allowCredentials(true)
// 設(shè)置允許的請(qǐng)求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 設(shè)置允許的header屬性
.allowedHeaders("*")
// 跨域允許時(shí)間
.maxAge(3600);
}
}②開(kāi)啟SpringSecurity的跨域訪問(wèn)
由于我們的資源都會(huì)收到SpringSecurity的保護(hù),所以想要跨域訪問(wèn)還要讓SpringSecurity運(yùn)行跨域訪問(wèn)。

六、其他權(quán)限校驗(yàn)方法
我們前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法進(jìn)行校驗(yàn)。
SpringSecurity還為我們提供了其它方法例如:hasAnyAuthority,hasRole,hasAnyRole等。
這里我們先不急著去介紹這些方法,我們先去理解hasAuthority的原理,然后再去學(xué)習(xí)其他方法你就更容易理解,而不是死記硬背區(qū)別。并且我們也可以選擇定義校驗(yàn)方法,實(shí)現(xiàn)我們自己的校驗(yàn)邏輯。
hasAuthority方法實(shí)際是執(zhí)行到了SecurityExpressionRoot的hasAuthority,大家只要斷點(diǎn)調(diào)試既可知道它內(nèi)部的校驗(yàn)原理。
它內(nèi)部其實(shí)是調(diào)用authentication的getAuthorities方法獲取用戶的權(quán)限列表。然后判斷我們存入的方法參數(shù)數(shù)據(jù)在權(quán)限列表中。
hasAnyAuthority方法可以傳入多個(gè)權(quán)限,只有用戶有其中任意一個(gè)權(quán)限都可以訪問(wèn)對(duì)應(yīng)資源。
@RequestMapping("/hello")
@PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
public String hello(){
return "hello";
}hasRole要求有對(duì)應(yīng)的角色才可以訪問(wèn),但是它內(nèi)部會(huì)把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對(duì)應(yīng)的權(quán)限也要有 ROLE_ 這個(gè)前綴才可以。
@RequestMapping("/hello")
@PreAuthorize("hasRole('system:dept:list')")
public String hello(){
return "hello";
}hasAnyRole 有任意的角色就可以訪問(wèn)。它內(nèi)部也會(huì)把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對(duì)應(yīng)的權(quán)限也要有 ROLE_ 這個(gè)前綴才可以。
@RequestMapping("/hello")
@PreAuthorize("hasAnyRole('admin','system:dept:list')")
public String hello(){
return "hello";
}七、自定義權(quán)限校驗(yàn)方法
我們也可以定義自己的權(quán)限校驗(yàn)方法,在@PreAuthorize注解中使用我們的方法。
import com.fox.domain.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.List;
@Component("ex")
public class FoxExpressionRoot {
public boolean hasAuthority(String authority){
//獲取當(dāng)前用戶的權(quán)限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
//判斷用戶權(quán)限集合中是否存在authority
return permissions.contains(authority);
}
}
在SPEL表達(dá)式中使用 @ex相當(dāng)于獲取容器中bean的名字未ex的對(duì)象。然后再調(diào)用這個(gè)對(duì)象的
hasAuthority方法:
@RequestMapping("/hello")
@PreAuthorize("@ex.hasAuthority('system:dept:list')")
public String hello(){
return "hello";
}八、基于配置的權(quán)限控制
我們也可以在配置類中使用使用配置的方式對(duì)資源進(jìn)行權(quán)限控制。

到此這篇關(guān)于SpringSecurity進(jìn)行認(rèn)證與授權(quán)的示例代碼的文章就介紹到這了,更多相關(guān)SpringSecurity 認(rèn)證與授權(quán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和鑒權(quán)全過(guò)程
- SpringBoot整合SpringSecurity和JWT和Redis實(shí)現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringBoot整合SpringSecurityOauth2實(shí)現(xiàn)鑒權(quán)動(dòng)態(tài)權(quán)限問(wèn)題
- SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的實(shí)現(xiàn)
- SpringSecurity動(dòng)態(tài)加載用戶角色權(quán)限實(shí)現(xiàn)登錄及鑒權(quán)功能
- springboot+jwt+springSecurity微信小程序授權(quán)登錄問(wèn)題
- SpringSecurity實(shí)現(xiàn)權(quán)限認(rèn)證與授權(quán)的使用示例
- springSecurity用戶認(rèn)證和授權(quán)的實(shí)現(xiàn)
- 深入淺析springsecurity入門登錄授權(quán)
- mall整合SpringSecurity及JWT實(shí)現(xiàn)認(rèn)證授權(quán)實(shí)戰(zhàn)
- SpringSecurity頁(yè)面授權(quán)與登錄驗(yàn)證實(shí)現(xiàn)(內(nèi)存取值與數(shù)據(jù)庫(kù)取值)
- SpringSecurity 鑒權(quán)與授權(quán)的具體使用
相關(guān)文章
Java Arrays.sort和Collections.sort排序?qū)崿F(xiàn)原理解析
這篇文章主要介紹了Java Arrays.sort和Collections.sort排序?qū)崿F(xiàn)原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
SpringCloud中的路由網(wǎng)關(guān)鑒權(quán)熔斷詳解
這篇文章主要介紹了SpringCloud中的路由網(wǎng)關(guān)鑒權(quán)熔斷詳解,Hystrix是一個(gè)用于處理分布式系統(tǒng)的延遲和容錯(cuò)的開(kāi)源庫(kù),在分布式系統(tǒng)里,許多依賴不可避免的會(huì)調(diào)用失敗,比如超時(shí)、異常等,需要的朋友可以參考下2024-01-01
SpringBoot 利用thymeleaf自定義錯(cuò)誤頁(yè)面
這篇文章主要介紹了SpringBoot 利用thymeleaf自定義錯(cuò)誤頁(yè)面,幫助大家更好的理解和使用springboot 框架,感興趣的朋友可以了解下2020-11-11
elasticsearch如何根據(jù)條件刪除數(shù)據(jù)
Elasticsearch是一個(gè)基于Apache Lucene?的開(kāi)源搜索引擎,無(wú)論在開(kāi)源還是專有領(lǐng)域,Lucene 可以被認(rèn)為是迄今為止最先進(jìn)、性能最好的、功能最全的搜索引擎庫(kù),這篇文章主要介紹了elasticsearch如何根據(jù)條件刪除數(shù)據(jù),需要的朋友可以參考下2023-03-03
listview點(diǎn)擊無(wú)效的處理方法(推薦)
下面小編就為大家?guī)?lái)一篇listview點(diǎn)擊無(wú)效的處理方法(推薦)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05
Java的JDBC編程使用之連接Mysql數(shù)據(jù)庫(kù)
這篇文章主要給大家介紹了關(guān)于Java的JDBC編程使用之連接Mysql數(shù)據(jù)庫(kù)的相關(guān)資料,JDBC是一種用于執(zhí)行SQL語(yǔ)句的Java?API,可以為多種關(guān)系數(shù)據(jù)庫(kù)提供統(tǒng)一訪問(wèn),需要的朋友可以參考下2023-12-12
TransmittableThreadLocal線程間傳遞邏輯示例解析
這篇文章主要介紹了TransmittableThreadLocal線程間傳遞邏輯示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06

