前言
其實挺早就想寫一篇關(guān)于jwt的博文去好好總結(jié)一下之前踩過的坑了就珠,但是事情有點太多了,一直沒抽出時間來寫醒颖,剛好現(xiàn)在有點時間可以好好靜下來寫一遍(可能)有點質(zhì)量的博文吧妻怎,畢竟一直都是看別人的博文去學習,我也好好寫一遍吧哈哈泞歉。既然如果偶然搜到這篇文章的話逼侦,我相信大家應該都了解了什么是jwt,比較想知道怎么使用springboot+spring-security去實現(xiàn)腰耙,當然也可以使用shiro榛丢,其實道理都差不多,可能看到標題可能會有疑問挺庞,為什么會有一個redis呢晰赞?這是我學習有關(guān)jwt相關(guān)知識的時候產(chǎn)生的一些問題,以及自己對這方面問題的一些解決方案选侨,接下來的文章我會詳細跟大家討論一下的掖鱼,歡迎大家也可以一起討論一下。(剛開始寫博客援制,寫的不好多多包涵)
由于簡書不能顯示目錄戏挡,不是特別方便,可以移步到我的個人博客進行查看晨仑。
看完這篇文章之后你可以知道
- 如何使用springboot褐墅,springSecurity拆檬,jwt實現(xiàn)基于token的權(quán)限管理
- 統(tǒng)一處理無權(quán)限請求的結(jié)果
JWT
再稍微提一提jwt吧,在前段時間有個小項目是前后端分離的妥凳,所以需要用到基于token的權(quán)限管理機制竟贯,所以就了解到了jwt這一個方案。不過關(guān)于這個方案逝钥,似乎沒有一個如何管理已經(jīng)生產(chǎn)的token的方法(如果有的話歡迎告知澄耍,我還不知道呢。晌缘。)一旦生成了一個token齐莲,就無法對該token進行任何操作,無法使該token失效磷箕,只有等到該token到了過期的時間點才失效选酗,這樣就會有一個很大的隱患。然后搜索了挺多相關(guān)的資料以及經(jīng)過相當長一段時間的思考決定使用redis去管理已經(jīng)生成的token岳枷,下面會詳細說一下芒填。
整理一下思路
創(chuàng)建一個新工程時,我們需要思考一下我們接下來需要的一些步驟空繁,需要做什么殿衰,怎么做。
- 搭建springboot工程
- 導入springSecurity跟jwt的依賴
- 用戶的實體類
- dao層
- service層(真正開發(fā)時再寫盛泡,這里就直接調(diào)用dao層操作數(shù)據(jù)庫)
- 實現(xiàn)UserDetailsService接口
- 實現(xiàn)UserDetails接口
- 驗證用戶登錄信息的攔截器
- 驗證用戶權(quán)限的攔截器
- springSecurity配置
- 認證的Controller以及測試的controller
- 測試
- 享受成功的喜悅
創(chuàng)建一個springboot工程
建議使用maven去構(gòu)建項目闷祥。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
實體類User
創(chuàng)建一個演示的實體類User,包含最基本的用戶名跟密碼傲诵,至于role干嘛用后面會提到
@Entity
@Table(name = "jd_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "role")
private String role;
// getter and setter...
}
JWT工具類
這里jwt我選擇的是jjwt凯砍,至于為什么,可能是因為我用的比較順手吧(:3」∠)
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
JwtTokenUtils
jwt工具類拴竹,對jjwt封裝一下方便調(diào)用
public class JwtTokenUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
private static final String SECRET = "jwtsecretdemo";
private static final String ISS = "echisan";
// 過期時間是3600秒悟衩,既是1個小時
private static final long EXPIRATION = 3600L;
// 選擇了記住我之后的過期時間為7天
private static final long EXPIRATION_REMEMBER = 604800L;
// 創(chuàng)建token
public static String createToken(String username, boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuer(ISS)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
}
// 從token中獲取用戶名
public static String getUsername(String token){
return getTokenBody(token).getSubject();
}
// 是否已過期
public static boolean isExpiration(String token){
return getTokenBody(token).getExpiration().before(new Date());
}
private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}
UserRepository
寫一個根據(jù)用戶名獲取用戶的方法,后續(xù)會用到
public interface UserRepository extends CrudRepository<User, Integer> {
User findByUsername(String username);
}
UserDetailsServiceImpl
使用springSecurity需要實現(xiàn)UserDetailsService
接口供權(quán)限框架調(diào)用栓拜,該方法只需要實現(xiàn)一個方法就可以了座泳,那就是根據(jù)用戶名去獲取用戶,那就是上面repository定義的方法了幕与,這里直接調(diào)用了挑势。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userRepository.findByUsername(s);
return new JwtUser(user);
}
}
由于接口方法需要返回一個UserDetails
類型的接口,所以這邊就再寫一個類去實現(xiàn)一下這個接口纽门。
JwtUser
實現(xiàn)這個接口需要實現(xiàn)幾個方法
public class JwtUser implements UserDetails {
private Integer id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public JwtUser() {
}
// 寫一個能直接使用user創(chuàng)建jwtUser的構(gòu)造器
public JwtUser(User user) {
id = user.getId();
username = user.getUsername();
password = user.getPassword();
authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
}
// 獲取權(quán)限信息薛耻,目前博主只會拿來存角色营罢。赏陵。
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
// 賬號是否未過期饼齿,默認是false,記得要改一下
@Override
public boolean isAccountNonExpired() {
return true;
}
// 賬號是否未鎖定蝙搔,默認是false缕溉,記得也要改一下
@Override
public boolean isAccountNonLocked() {
return true;
}
// 賬號憑證是否未過期,默認是false吃型,記得還要改一下
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 這個有點抽象不會翻譯证鸥,默認也是false,記得改一下
@Override
public boolean isEnabled() {
return true;
}
// 我自己重寫打印下信息看的
@Override
public String toString() {
return "JwtUser{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", authorities=" + authorities +
'}';
}
}
配置攔截器
可以說到目前為止這是最復雜的一個步驟勤晚,其實搞清楚了還是挺簡單的枉层,網(wǎng)上挺多人都更傾向于使用shiro,但是偶爾也要嘗試一下新東西的嘛赐写,但是當時我在摸索的時候遇到挺多坑鸟蜡,當時也已經(jīng)到了思考人生的地步了 框架不是為了簡化開發(fā)嗎!為什么挺邀!明明jwt加上權(quán)限框架是雙倍的快樂揉忘!為什么會這樣!(╯°口°)╯(┴—┴
回到正題端铛,到底要怎么配置呢泣矛?使用過shiro的人會知道,鑒權(quán)的話需要自己實現(xiàn)一個realm禾蚕,重寫兩個方法您朽,第一是用戶驗證,第二是鑒權(quán)换淆。在spring-security中也不例外虚倒,這邊需要實現(xiàn)兩個過濾器。使用JWTAuthenticationFilter
去進行用戶賬號的驗證产舞,使用JWTAuthorizationFilter
去進行用戶權(quán)限的驗證魂奥。
JWTAuthenticationFilter
JWTAuthenticationFilter
繼承于UsernamePasswordAuthenticationFilter
該攔截器用于獲取用戶登錄的信息,只需創(chuàng)建一個token
并調(diào)用authenticationManager.authenticate()
讓spring-security去進行驗證就可以了易猫,不用自己查數(shù)據(jù)庫再對比密碼了耻煤,這一步交給spring去操作。
這個操作有點像是shiro的subject.login(new UsernamePasswordToken())
准颓,驗證的事情交給框架哈蝇。
獻上這一部分的代碼。
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 從輸入流中獲取到登錄的信息
try {
LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
// 成功驗證后調(diào)用的方法
// 如果驗證成功攘已,就生成token并返回
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 查看源代碼會發(fā)現(xiàn)調(diào)用getPrincipal()方法會返回一個實現(xiàn)了`UserDetails`接口的對象
// 所以就是JwtUser啦
JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
System.out.println("jwtUser:" + jwtUser.toString());
String token = JwtTokenUtils.createToken(jwtUser.getUsername(), false);
// 返回創(chuàng)建成功的token
// 但是這里創(chuàng)建的token只是單純的token
// 按照jwt的規(guī)定炮赦,最后請求的格式應該是 `Bearer token`
response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
}
// 這是驗證失敗時候調(diào)用的方法
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.getWriter().write("authentication failed, reason: " + failed.getMessage());
}
}
JWTAuthorizationFilter
驗證成功當然就是進行鑒權(quán)了,每一次需要權(quán)限的請求都需要檢查該用戶是否有該權(quán)限去操作該資源样勃,當然這也是框架幫我們做的吠勘,那么我們需要做什么呢性芬?很簡單,只要告訴spring-security該用戶是否已登錄剧防,是什么角色植锉,擁有什么權(quán)限就可以了。
JWTAuthenticationFilter
繼承于BasicAuthenticationFilter
峭拘,至于為什么要繼承這個我也不太清楚了俊庇,這個我也是網(wǎng)上看到的其中一種實現(xiàn),實在springSecurity苦手鸡挠,不過我覺得不繼承這個也沒事呢(實現(xiàn)以下filter接口或者繼承其他filter實現(xiàn)子類也可以吧)只要確保過濾器的順序辉饱,JWTAuthorizationFilter
在JWTAuthenticationFilter
后面就沒問題了。
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
// 如果請求頭中沒有Authorization信息則直接放行了
if (tokenHeader == null || tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果請求頭中有token拣展,則進行解析鞋囊,并且設(shè)置認證信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
super.doFilterInternal(request, response, chain);
}
// 這里從token中獲取用戶信息并新建一個token
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
String username = JwtTokenUtils.getUsername(token);
if (username != null){
return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
}
return null;
}
}
配置SpringSecurity
到這里基本操作都寫好啦,現(xiàn)在就需要我們將這些辛苦寫好的“組件”組合到一起發(fā)揮作用了瞎惫,那就需要配置了溜腐。需要開啟一下注解@EnableWebSecurity
然后再繼承一下WebSecurityConfigurerAdapter
就可以啦,springboot就是可以為所欲為~
@EnableWebSecurity
// 至于為什么要配置這個瓜喇,嘿嘿挺益,賣個關(guān)子
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
// 因為UserDetailsService的實現(xiàn)類實在太多啦,這里設(shè)置一下我們要注入的實現(xiàn)類
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
// 加密密碼的乘寒,安全第一嘛~
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
// 測試用資源望众,需要驗證了的用戶才能訪問
.antMatchers("/tasks/**").authenticated()
// 其他都放行了
.anyRequest().permitAll()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
AuthController
連配置都搞定了,那么問題來了伞辛,沒有賬號密碼呢烂翰。所以寫一個注冊的控制器,這個就不是難事啦
@RestController
@RequestMapping("/auth")
public class AuthController {
// 為了減少篇幅就不寫service接口了
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@PostMapping("/register")
public String registerUser(@RequestBody Map<String,String> registerUser){
User user = new User();
user.setUsername(registerUser.get("username"));
// 記得注冊的時候把密碼加密一下
user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password")));
user.setRole("ROLE_USER");
User save = userRepository.save(user);
return save.toString();
}
}
等等!注冊是有了,那登錄在哪呢谢肾?我們看一下UsernamePasswordAuthenticationFilter
的源代碼
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
可以看出來默認是/login
嘉抒,所以登錄直接使用這個路徑就可以啦~當然也可以自定義
只需要在JWTAuthenticationFilter
的構(gòu)造方法中加入下面那一句話就可以啦
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/auth/login");
}
所以現(xiàn)在認證的路徑統(tǒng)一了一下也是挺好的~看起來相當舒服了
注冊:/auth/register
登錄:/auth/login
TaskController
當然注冊登錄都完成了折联,那就是寫一個測試控制器,一個需要權(quán)限的控制器去測試了,為了控制一下文章篇幅,寫了一個比較簡單的控制器作為演示
@RestController
@RequestMapping("/tasks")
public class TaskController {
@GetMapping
public String listTasks(){
return "任務列表";
}
@PostMapping
public String newTasks(){
return "創(chuàng)建了一個新的任務";
}
@PutMapping("/{taskId}")
public String updateTasks(@PathVariable("taskId")Integer id){
return "更新了一下id為:"+id+"的任務";
}
@DeleteMapping("/{taskId}")
public String deleteTasks(@PathVariable("taskId")Integer id){
return "刪除了id為:"+id+"的任務";
}
}
測試
到這里基本操作都做好了毁葱,可以去測試一下了,這里使用的是postman比較直觀明了了贰剥。下面先注冊一下賬號倾剿,這里返回了插入了數(shù)據(jù)庫之后的用戶實體,所以注冊是成功了
接下來先測試一下先不登錄訪問一下我們的tasks蚌成,這里理所當然403無權(quán)限訪問了
然后終于能登錄了前痘,接下來嘗試一下登錄之后再次訪問tasks看看是什么結(jié)果
發(fā)送了登錄請求之后查看響應頭凛捏,能看到我們生成后的token,那就是登錄成功了
接下來只需要把該響應頭添加到我們的請求頭上去际度,這里需要把
Bearer[空格]
去掉葵袭,注意Bearer后的空格也要去掉涵妥,因為postman再選了BearerToken之后會自動在token前面再加一個Bearer再次訪問一下tasks乖菱,結(jié)果理想當然的是成功啦~
初期總結(jié)
到這里我們一個基礎(chǔ)的Springboot+SpringSecurity+Jwt已經(jīng)搭建好了。
到這里一個基本的jwt已經(jīng)實現(xiàn)了蓬网,但是總覺得哪里不對呢窒所,寫了這么多才只是登錄成功了?權(quán)限管理呢帆锋?token管理呢吵取?
確實,看一下上面的代碼锯厢。在實現(xiàn)UserDetails
接口的時候?qū)懥艘恍┢婀值臇|西皮官,就是這個getAuthorities
方法啦。
這是springSecurity用來獲取用戶權(quán)限的方法实辑。
在User類中寫得role
在這里就能排上用場了捺氢,這里將要實現(xiàn)的權(quán)限管理是基于角色的權(quán)限管理,再細顆粒的博主就不會啦哈哈哈剪撬,但還是可以看一看的摄乒。
// 寫一個能直接使用user創(chuàng)建jwtUser的構(gòu)造器
public JwtUser(User user) {
id = user.getId();
username = user.getUsername();
password = user.getPassword();
// 這里只存儲了一個角色的名字
authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
}
// 獲取權(quán)限信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
在springSecurity里建議角色名稱改成ROLE_
統(tǒng)一前綴的角色,例如ROLE_USER,ROLE_ADMIN,ROLE_XXX
残黑,至于為什么馍佑,后面會提到的,先不急梨水,這里先這樣干著拭荤。
基于角色的權(quán)限管理
到底怎么基于角色的權(quán)限管理呢,這個只需要告訴權(quán)限框架該用戶擁有什么角色就可以了疫诽。但是吧要怎么告訴框架我什么角色呢穷劈。我們理一下如何實現(xiàn)基于角色的權(quán)限管理的思路
- 用戶驗證成功,根據(jù)用戶名以及過期時間生成token
- 權(quán)限驗證踊沸,假如能從token中獲取用戶名就該token驗證成功
- 創(chuàng)建一個
UsernamePasswordAuthenticationToken
該token包含用戶的角色信息歇终,而不是一個空的ArrayList
,查看一下源代碼是有以下一個構(gòu)造方法的逼龟。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
好了评凝,接下來要怎么辦呢,可以往上滾動一下腺律,再看一眼JWTAuthorizationFilter
中鑒權(quán)的邏輯
- 檢查請求頭中是否存在
Authorization
奕短,如果沒有直接放宜肉,如果有就對token進行解析 - 解析token,檢查是否能從token中取出username翎碑,如果有就算成功了
- 再根據(jù)該username創(chuàng)建一個
UsernamePasswordAuthenticationToken
對象就算成功了
可這發(fā)現(xiàn)根本就不關(guān)role
什么事啊
User user = userRepository.findByUsername("username");
String role = user.getRole();
這還不簡單谬返!這不就完事了嘛!
可這不現(xiàn)實啊日杈,每一次請求都要查詢一下數(shù)據(jù)庫這種開銷這么大的操作當然是不行的遣铝。
思考一下,為什么是使用jwt而不是一個簡簡單單的UUID
作為token呢莉擒。
jwt是由三部分組成的:
- 第一部分我們稱它為頭部(header)
- 第二部分我們稱其為載荷(payload)
- 第三部分是簽證(signature)
我們這里準備使用它的第二部分酿炸,使用payload去存儲我們的用戶角色信息,由于第一第二部分都是公開的涨冀,任何人都能知道里面的信息填硕,不建議存儲一些比較敏感的數(shù)據(jù),但是存放角色信息還是沒有問題的鹿鳖。
改造一下JwtTokenUtils
// 添加角色的key
private static final String ROLE_CLAIMS = "rol";
// 修改一下創(chuàng)建token的方法
public static String createToken(String username, String role, boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
// 這里要早set一點扁眯,放到后面會覆蓋別的字段
.setClaims(map)
.setIssuer(ISS)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
}
修改JWTAuthenticationFilter
JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
boolean isRemember = rememberMe.get() == 1;
String role = "";
// 因為在JwtUser中存了權(quán)限信息,可以直接獲取翅帜,由于只有一個角色就這么干了
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities){
role = authority.getAuthority();
}
// 根據(jù)用戶名姻檀,角色創(chuàng)建token
String token = JwtTokenUtils.createToken(jwtUser.getUsername(), role, isRemember);
修改JWTAuthorizationFilter
// 這里從token中獲取用戶信息并新建一個token
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
String username = JwtTokenUtils.getUsername(token);
String role = JwtTokenUtils.getUserRole(token);
if (username != null){
return new UsernamePasswordAuthenticationToken(username, null,
Collections.singleton(new SimpleGrantedAuthority(role))
);
}
return null;
}
到這里基本上修改已經(jīng)完成了,接下來就可以測試一下了藕甩,再配置一下springSecurity
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
// 測試用資源施敢,需要驗證了的用戶才能訪問
.antMatchers("/tasks/**").authenticated()
// 需要角色為ADMIN才能刪除該資源
.antMatchers(HttpMethod.DELETE, "/tasks/**").hasAuthority("ROLE_ADMIN")
// 其他都放行了
.anyRequest().permitAll()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
由于更新了token的生成方式,所以需要重新登錄一下獲取新的token
接下來可以測試了狭莱,繼續(xù)使用postman對tasks資源進行刪除僵娃,顯然不行。
試試看獲取該資源會怎么樣腋妙,獲取tasks資源是沒有問題的默怨。
接下來重頭戲來了
先在數(shù)據(jù)庫里手動將admin的角色改成ROLE_ADMIN
修改完之后再登錄一下獲取新的token,再去嘗試一下刪除tasks資源
啪啪啪 成功啦~
到這里位置骤素,基于角色的權(quán)限管理基本操作都做了一遍了匙睹,現(xiàn)在來解答一下上面挖的一些坑
- 為什么要以
ROLE_
作為前綴 - springSecurity中配置的注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
是干嘛用的
第一個問題:
我們在springSecurity中配置了這樣一句,意思是只有角色為ROLE_ADMIN
才有權(quán)限刪除該資源
.antMatchers(HttpMethod.DELETE, "/tasks/**").hasAuthority("ROLE_ADMIN")
假如我們使用了ROLE_
作為前綴就能這樣寫了~是不是很方便呢哈哈
.antMatchers(HttpMethod.DELETE, "/tasks/**").hasRole("ADMIN")
第二個問題:
除了在springSecurity中配置訪問權(quán)限济竹,還有這種方式啦痕檬,也是十分的方便呢。但是如果要使用這用的方式就需要配置上那個注解啦送浊,不然雖然寫了下面的注解但是是不會生效的梦谜。
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public String newTasks(){
return "創(chuàng)建了一個新的任務";
}
統(tǒng)一結(jié)果處理
當然會有一些需求是要統(tǒng)一處理被403響應的事件,很簡單,只要新建一個類JWTAuthenticationEntryPoint
實現(xiàn)一下接口AuthenticationEntryPoint
就可以了
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
String reason = "統(tǒng)一處理唁桩,原因:"+authException.getMessage();
response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
}
}
再配置一下springSecurity
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
// 測試用資源闭树,需要驗證了的用戶才能訪問
.antMatchers("/tasks/**").authenticated()
.antMatchers(HttpMethod.DELETE, "/tasks/**").hasRole("ADMIN")
// 其他都放行了
.anyRequest().permitAll()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 加一句這個
.exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint());
}
這是統(tǒng)一處理后的結(jié)果
享受成功的喜悅
到這里一個較為完善的權(quán)限管理已經(jīng)實現(xiàn)啦,如果哪里有不足或者出現(xiàn)錯誤可以告訴一下我荒澡,或者可以到GitHub上提個issue一起討論下报辱。
代碼地址
Github: springboot-jwt-demo
代碼里也有挺多的注釋,可以看一看单山,如果覺得這篇文章幫助到你了可以到github點個小星星鼓勵一下博主~
結(jié)語
至于為什么沒有redis碍现,沒有token管理,因為在我寫這篇文章的時候想了很多饥侵,感覺我現(xiàn)在的解決方案也不是特別好鸵赫,如果想知道的話可以到GitHub上找我衣屏,一起討論下躏升。