SpringSecurity角色權(quán)限控制(SpringBoot+SpringSecurity+JWT)
一、項(xiàng)目介紹
通過springboot整合jwt和security,以用戶名/密碼的方式進(jìn)行認(rèn)證和授權(quán)。認(rèn)證通過jwt+數(shù)據(jù)庫的,授權(quán)這里使用了兩種方式,分別是SpringSecurity自帶的hasRole方法+SecurityConfig 和 我們自定義的permission+@PreAuthorize注解。
二、SpringSecurity簡介
SpringSecurity中的幾個(gè)重要組件:
1.SecurityContextHolder(class)
用來存儲(chǔ)和獲取當(dāng)前線程關(guān)聯(lián)的 SecurityContext 對象的類。
其中有兩種 SecurityContext 模式:
- MODE_THREADLOCAL:將 SecurityContext 對象存儲(chǔ)到當(dāng)前線程中,只在當(dāng)前線程中可見。多線程時(shí),每個(gè)線程的 SecurityContext 對象都是獨(dú)立的。
- MODE_INHERITABLETHREADLOCAL:將 SecurityContext 對象存儲(chǔ)到當(dāng)前線程中,對當(dāng)前線程和子線程都可見。也就是說,在當(dāng)前線程中存儲(chǔ)的 SecurityContext 對象可以被傳遞給子線程使用。
表示用戶已通過身份驗(yàn)證的最簡單方法就是設(shè)置 SecurityContextHolder!
2.SecurityContext(Interface)
用來存儲(chǔ)當(dāng)前已經(jīng)被認(rèn)證的用戶,包含了當(dāng)前執(zhí)行操作的線程上下文信息以及用戶認(rèn)證和授權(quán)信息。包含Authentication。
3.Authentication(Interface)
存儲(chǔ)了當(dāng)前正在執(zhí)行操作的用戶的身份驗(yàn)證信息,包括用戶名、密碼、權(quán)限,可以作為AuthenticationManager的輸入,它包含principal、credentials、authorities。
- principal:用戶的標(biāo)識(shí)。通常情況下是UserDetails接口的一個(gè)實(shí)例。
- credentials:用戶的密碼。多數(shù)情況下,為確保密碼不被泄露,會(huì)在用戶身份驗(yàn)證之后被清除。
- authorities:用戶擁有的權(quán)限。
Authentication 接口的常用實(shí)現(xiàn)類有以下幾種:
- AnonymousAuthenticationToken:匿名用戶的身份驗(yàn)證信息。
- UsernamePasswordAuthenticationToken:用戶名/密碼的身份驗(yàn)證信息。
- RememberMeAuthenticationToken:用于“記住我”功能的身份驗(yàn)證信息。
- PreAuthenticatedAuthenticationToken:基于預(yù)先認(rèn)證信息的身份驗(yàn)證。
通常情況下,用戶在進(jìn)行登錄時(shí)需要通過身份驗(yàn)證,當(dāng)身份驗(yàn)證成功時(shí),就會(huì)通過 Authentication 接口封裝用戶的身份信息。在后續(xù)的操作中,認(rèn)證后的用戶可以通過 SecurityContextHolder 獲取 Authentication 對象,并根據(jù)其中的信息獲得用戶的身份信息及相應(yīng)的權(quán)限等。
對比SecurityContext和Authentication:
Authentication 是一個(gè)封裝了用戶身份認(rèn)證信息的對象,表示用戶已經(jīng)通過了驗(yàn)證。
SecurityContext 則是一個(gè)上下文類對象,用于保存和獲取當(dāng)前線程關(guān)聯(lián)的上下文信息,包括了 Authentication 對象。
4.AuthenticationManager(Interface)
定義了用戶身份驗(yàn)證的api接口(例如將用戶名和密碼和數(shù)據(jù)庫進(jìn)行比對)。可以接收一個(gè)Authentication對象作為入?yún)?,?yàn)證成功后會(huì)返回已被驗(yàn)證的Authentication對象。
5.GrantedAuthority(Interface)
授權(quán)信息以GrantedAuthority的形式存儲(chǔ)在Authentication對象中,GrantedAuthority接口表示一個(gè)授權(quán)(權(quán)限)對象,包含一個(gè)字符串類型的授權(quán)名字(authority name)。授權(quán)名字通常是一個(gè)表示權(quán)限的字符串,例如"ROLE_ADMIN"、"ROLE_USER"等
二、整理思路
- 搭建springboot項(xiàng)目,導(dǎo)入相關(guān)依賴
- 在數(shù)據(jù)庫導(dǎo)入sql創(chuàng)建用戶表
- 創(chuàng)建幾個(gè)關(guān)于User的對象便于數(shù)據(jù)傳輸
- dao層開發(fā)(對用戶信息增刪改查)
- 實(shí)現(xiàn)UserDetails接口和UserDetailService接口
- 自定義實(shí)現(xiàn)校驗(yàn)token的攔截器JwtAuthenticationTokenFilter
- 自定義實(shí)現(xiàn)用戶登錄校驗(yàn)的攔截器JWTAuthenticationFilter
- service層開發(fā)(包括PermissionService和UserService)
- 創(chuàng)建測試Controller
- 實(shí)現(xiàn)SpringSecurity的配置類SecurityConfig
- 通過postman進(jìn)行測試
三、具體實(shí)現(xiàn)步驟
1.項(xiàng)目主要相關(guān)依賴
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.3.12.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.3.12.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> </dependencies>
2.用戶表
創(chuàng)建一個(gè)名為security_jwt_demo的數(shù)據(jù)庫,導(dǎo)入項(xiàng)目根目錄下/db/db.sql文件即可。
3.項(xiàng)目中用到的幾個(gè)user相關(guān)對象
(1)User實(shí)體
對應(yīng)數(shù)據(jù)庫中的user表
@Data public class User { private Long id; private String username; private String password; private String permission; private String role; @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + ", permission='" + permission + '\'' + ", role='" + role + '\'' + '}'; } }
(2)LoginUser
UserDetails的實(shí)現(xiàn)類,使用用戶名/密碼驗(yàn)證時(shí)需要用到其作為返回值。這個(gè)類中包含了用戶名、密碼和當(dāng)前登錄用戶所具備的權(quán)限。
@Data public class LoginUser implements UserDetails { private Long id; private String username; private String password; //通過自定義方式進(jìn)行授權(quán) private Set<String> permissions = new HashSet<String>(); //通過springSecurity進(jìn)行授權(quán) private Collection<? extends GrantedAuthority> authorities; public LoginUser(){} public LoginUser(User user,Collection<? extends GrantedAuthority> authorities) { id = user.getId(); username = user.getUsername(); password = user.getPassword(); permissions.add(user.getPermission()); this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @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; } }
(3)UserVO
主要用來登錄時(shí)從輸入流中獲取登錄用戶的信息,對應(yīng)了前端傳遞的參數(shù),包括用戶名、密碼、記住我等屬性。
@Data public class UserVO { private String username; private String password; private Integer rememberMe; }
4.dao層開發(fā)
創(chuàng)建mapper接口并配置對應(yīng)的mapper.xml
@Mapper public interface UserMapper { //根據(jù)用戶名獲取用戶 User getByName(String username); //根據(jù)用戶id獲取用戶權(quán)限 List<String> getPermissionById(Long id); //新增一個(gè)用戶 int insertUser(User user); }
5.實(shí)現(xiàn)UserDetail接口和UserDetailService接口
首先說一下為什么要實(shí)現(xiàn)這兩個(gè)接口。
SpringSecurity提供了多種身份驗(yàn)證的方式,在這里我們使用的是用戶名/密碼的方式進(jìn)行驗(yàn)證,而如果要使用這種方式進(jìn)行驗(yàn)證的話,我們需要實(shí)現(xiàn)UserDetailService接口中的loadUserByUsername方法,這個(gè)方法用來從數(shù)據(jù)庫中進(jìn)行查詢用戶,然后和傳入的用戶密碼進(jìn)行比對。這個(gè)方法會(huì)返回一個(gè)UserDetails對象。
@Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.getByName(username); List<SimpleGrantedAuthority> authorities = new ArrayList<>(); return new LoginUser(user,authorities); } }
對于UserDetailService接口,SpringSecurity提供了基于內(nèi)存和JDBC的兩種驗(yàn)證方式,默認(rèn)是JDBC的方式。我們也可以通過自定義實(shí)現(xiàn)UserDetailService接口,來達(dá)到自定義身份驗(yàn)證的結(jié)果。這里我們使用的是自定義身份驗(yàn)證的方式。
對于UserDetails,在Spring Security中,UserDetails接口是表示用戶信息的規(guī)范。該接口表示應(yīng)用程序中的用戶,并提供有關(guān)用戶的基本信息,如用戶名、密碼、角色、權(quán)限等,因此我們需要有一個(gè)類似用戶的對象來實(shí)現(xiàn)該接口(LoginUser)。
6.JwtAuthenticationTokenFilter
檢驗(yàn)用戶token的過濾器。對于客戶端發(fā)出的請求,首先對用戶的token進(jìn)行校驗(yàn),如果token不合法表示當(dāng)前用戶未登錄,繼續(xù)執(zhí)行其他過濾器的邏輯;如果token合法則設(shè)置SecurityContextHolder表示用戶已被認(rèn)證。
/** * token過濾器 驗(yàn)證token有效性 */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired UserMapper userMapper; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = request.getHeader(JwtTokenUtil.TOKEN_HEADER); if (StringUtils.isBlank(token) || !token.startsWith(JwtTokenUtil.TOKEN_PREFIX)){ chain.doFilter(request,response); return; } try { //如果能獲取到token則Authentication進(jìn)行設(shè)置,表示已認(rèn)證 SecurityContextHolder.getContext().setAuthentication(getAuthentication(token)); } catch (Exception e) { e.printStackTrace(); } //繼續(xù)執(zhí)行其他過濾器的邏輯 chain.doFilter(request,response); } private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) throws Exception { String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX,""); //判斷token是否過期 boolean expiration = JwtTokenUtil.isExpiration(token); if (expiration){ throw new Exception("過期了"); }else{ String username = JwtTokenUtil.getUsername(token); User user = userMapper.getByName(username); List<String> permissions = userMapper.getPermissionById(user.getId()); LoginUser loginUser = new LoginUser(user, Collections.singleton(new SimpleGrantedAuthority(user.getRole()))); loginUser.setPermissions(new HashSet<>(permissions)); //新建一個(gè)UsernamePasswordAuthenticationToken用來設(shè)置Authentication UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); return authenticationToken; } } }
7.JWTAuthenticationFilter
用戶登錄時(shí)對用戶名密碼進(jìn)行校驗(yàn)的過濾器,在token校驗(yàn)過濾器之后執(zhí)行。在該過濾器中會(huì)對用戶名密碼進(jìn)行比對,校驗(yàn)成功后返回一個(gè)token給客戶端,下次客戶端訪問時(shí)在請求頭帶上此token代表該用戶已經(jīng)被認(rèn)證。
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private ThreadLocal<Integer> rememberMe = new ThreadLocal<>(); private AuthenticationManager authenticationManager; public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; super.setFilterProcessesUrl("/auth/login"); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 從輸入流中獲取到登錄的信息 try { UserVO vo = new ObjectMapper().readValue(request.getInputStream(), UserVO.class); rememberMe.set(vo.getRememberMe() == null ? 0 : vo.getRememberMe()); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(vo.getUsername(), vo.getPassword(), new ArrayList<>()) ); } catch (IOException e) { e.printStackTrace(); return null; } } // 成功驗(yàn)證后調(diào)用的方法 // 如果驗(yàn)證成功,就生成token并返回 @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { LoginUser loginUser = (LoginUser) authResult.getPrincipal(); System.out.println("loginUser:" + loginUser.toString()); boolean isRemember = rememberMe.get() == 1; String role = ""; String token = JwtTokenUtil.createToken(loginUser.getUsername(), role, isRemember); /* 返回創(chuàng)建成功的token 但是這里創(chuàng)建的token只是單純的token 按照jwt的規(guī)定,最后請求的時(shí)候應(yīng)該是 `Bearer token`*/ response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { response.getWriter().write("authentication failed, reason: " + failed.getMessage()); } }
8.service層開發(fā)
由于業(yè)務(wù)邏輯比較簡單,我們在service層中主要實(shí)現(xiàn)自定義授權(quán)的邏輯,用戶相關(guān)的Service不做實(shí)現(xiàn)。
自定義授權(quán)的實(shí)現(xiàn)如下,首先在數(shù)據(jù)庫的user表中有個(gè)string類型的permission字段,代表用戶所擁有的權(quán)限。在進(jìn)行授權(quán)時(shí)檢查用戶權(quán)限屬性是否包含該權(quán)限,如果包含則表示當(dāng)前用戶具有訪問權(quán)限。
/** * 自定義權(quán)限實(shí)現(xiàn),ss取自SpringSecurity首字母 */ @Service("ss") public class PermissionService { public boolean hasPer(String permission) throws Exception { if (StringUtils.isBlank(permission)){ return false; } LoginUser loginUser = SecurityUtil.getLoginUser(); if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions())) { return false; } return loginUser.getPermissions().contains(StringUtils.trim(permission)); } }
9.Controller層開發(fā)
我們用到的Controller主要有兩個(gè),一個(gè)是用戶相關(guān)的,一個(gè)是進(jìn)行測試的接口。
對于我們進(jìn)行授權(quán)測試的接口,在使用自定義授權(quán)邏輯時(shí)(PermissionService),要配合@PreAuthorize注解實(shí)現(xiàn)(createJob方法),對應(yīng)的JobController如下:
@RestController @RequestMapping("/jobs") public class JobController { @GetMapping("/list") public String listJobs(){ System.out.println("接收到請求..."); return "展示所有任務(wù)"; } //通過PermissionService自定義授權(quán)實(shí)現(xiàn) @PostMapping("/create") @PreAuthorize("@ss.hasPer('job:add')") public String createJob(){ return "創(chuàng)建一個(gè)新任務(wù)"; } //通過SpringSecurity配合用戶角色(role字段)實(shí)現(xiàn)權(quán)限管理 @DeleteMapping("/delete") public String deleteJob(){ return "刪除一個(gè)任務(wù)"; } }
對于用戶相關(guān)的Controller,我們只需要寫一個(gè)注冊方法就行了,如下所示:
@RestController @RequestMapping("/") public class UserController { @Autowired UserMapper userMapper; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @PostMapping("/register") public String register(@RequestBody Map<String,String> registerUser){ User user = new User(); user.setUsername(registerUser.get("username")); //對密碼進(jìn)行一下加密 user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password"))); user.setPermission(registerUser.get("permission")); user.setRole(registerUser.get("role")); userMapper.insertUser(user); return user.toString(); } }
為什么不需要注冊接口呢?是因?yàn)閁sernamePasswordAuthenticationFilter已經(jīng)幫我們實(shí)現(xiàn)了,默認(rèn)是"/login"
public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); }
這里我們也可以對這個(gè)路徑進(jìn)行修改,如下所示:
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; super.setFilterProcessesUrl("/auth/login"); }
10.通過SecurityConfig類對SpringSecurity進(jìn)行配置
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired @Qualifier("userDetailServiceImpl") UserDetailsService userDetailsService; @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() // 測試用資源,需要驗(yàn)證了的用戶才能訪問 .antMatchers("/jobs/create").authenticated() //只有角色為admin的用戶才能進(jìn)行刪除 .antMatchers(HttpMethod.DELETE,"/jobs/delete").hasRole("ADMIN") // 其他請求都放行了 .anyRequest().permitAll() .and() .addFilter(new JWTAuthenticationFilter(authenticationManager())) // 不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //將驗(yàn)證token的過濾器添加在驗(yàn)證用戶名/密碼的過濾器之前 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } @Bean CorsConfigurationSource corsConfigurationSource(){ final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**",new CorsConfiguration().applyPermitDefaultValues()); return source; } }
到這里就全部完成了!讓我們測試一下是否生效。
11.postman測試
這是數(shù)據(jù)庫表中原有的角色和權(quán)限:
(1)測試注冊接口
可以看到是可以注冊成功的,但是我們這里使用原有user1和user2進(jìn)行權(quán)限測試(偷個(gè)懶)
(2)測試登錄接口
由于我們之前將原有的登錄接口從"/login"改為了"/auth/login",這里需要注意一下。
登錄成功后的ResponseBody是空的,響應(yīng)頭中有token代表已經(jīng)登錄成功了。我們需要從header中獲取該token,后續(xù)請求需要用到。這里展示了user2,user1也是一樣的。
(3)測試創(chuàng)建任務(wù)接口
由于創(chuàng)建任務(wù)需要"job:add"權(quán)限,查看數(shù)據(jù)庫user2是有該權(quán)限的,user1沒有。
將剛才登錄獲得的token添加到參數(shù)中:
然后發(fā)現(xiàn)就可以創(chuàng)建成功了
(4)測試刪除任務(wù)接口
刪除任務(wù)需要用戶角色是admin,故user2是無法進(jìn)行刪除的。
(5)我們按照剛才的流程對user1進(jìn)行測試,來達(dá)到對比的效果。
登錄:
創(chuàng)建任務(wù):
刪除任務(wù):
對于查詢接口不需要進(jìn)行認(rèn)證和授權(quán),也就是說不需要登錄就能訪問:
到此這篇關(guān)于SpringSecurity角色權(quán)限控制(SpringBoot+SpringSecurity+JWT)的文章就介紹到這了,更多相關(guān)SpringSecurity角色權(quán)限內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot集成?JWT實(shí)現(xiàn)用戶登錄認(rèn)證的項(xiàng)目實(shí)踐
- SpringBoot結(jié)合JWT實(shí)現(xiàn)用戶登錄、注冊、鑒權(quán)
- springBoot整合jwt實(shí)現(xiàn)token令牌認(rèn)證的示例代碼
- springboot中通過jwt令牌校驗(yàn)及前端token請求頭進(jìn)行登錄攔截實(shí)戰(zhàn)記錄
- SpringBoot整合JWT(JSON?Web?Token)生成token與驗(yàn)證的流程及示例
- Springboot+jwt實(shí)現(xiàn)在線用戶功能(示例代碼)
相關(guān)文章
Spring?Security中如何獲取AuthenticationManager對象
有時(shí)需要使用AuthenticationManager(以下簡稱Manager)對象,可是這個(gè)對象不是Bean,沒有直接保存在Spring的Bean庫中,那么如何獲取Spring Security中的這個(gè)對象呢,需要的朋友可以參考下2022-11-11關(guān)于Spring中Bean的創(chuàng)建進(jìn)行更多方面的控制
今天小編就為大家分享一篇關(guān)于關(guān)于Spring中Bean的創(chuàng)建進(jìn)行更多方面的控制,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-01-01SpringCloud分布式鏈路追蹤組件Sleuth配置詳解
這篇文章主要介紹了SpringCloud鏈路追蹤組件Sleuth配置方法解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-11-11mybatis如何對大量數(shù)據(jù)的游標(biāo)查詢
這篇文章主要介紹了mybatis如何對大量數(shù)據(jù)的游標(biāo)查詢問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01Spring中的@Autowired注解深入解析與實(shí)戰(zhàn)指南
本文介紹了Spring框架中的@Autowired注解,詳細(xì)講解了其基本用法、高級(jí)用法以及實(shí)際應(yīng)用場景,通過@Autowired注解,Spring容器可以自動(dòng)將依賴的Bean注入到目標(biāo)Bean中,從而簡化代碼并提高可維護(hù)性,需要的朋友可以參考下2024-11-11mybatis配置Mapper.xml文件時(shí)遇到的問題及解決
這篇文章主要介紹了mybatis配置Mapper.xml文件時(shí)遇到的問題及解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01