SpringBoot整合SpringSecurity和JWT的示例
JWT
1.介紹:
全稱 JSON Web Token
,通過數(shù)字簽名的方式,以JSON
為載體,在不同的服務終端之間安全的傳遞信息。
常用于授權認證,用戶登錄后的每個請求都包含JWT
,后端處理請求之前都要進行校驗。
2.組成:
Header
:數(shù)據(jù)頭,令牌類型和加密算法
Payload
:負載,請求體和其他數(shù)據(jù)
Signature
:簽名,把頭部的base64UrlEncode與負載的base64UrlEncode拼接起來再進行HMACSHA256加密
用戶認證流程
1.用戶提交登錄表單(用戶名和密碼)
2.后端校驗成功后生成JWT,通過response的header返回給前端
3.前端將
JWT
保存到LocalStorage
中4.之后所有的請求中請求頭都攜帶
JWT
進行身份認證
Spring Security(安全框架)
1、介紹Spring Security
是一個能夠為基于Spring
的企業(yè)應用系統(tǒng)提供聲明式的安全訪問控制解決方案的安全框架。
如果項目中需要進行權限管理,具有多個角色和多種權限,我們可以使用Spring Security。
采用的是責任鏈的設計模式,是一堆過濾器鏈的組合,它有一條很長的過濾器鏈。
2、功能Authentication
(認證),就是用戶登錄Authorization
(授權),判斷用戶擁有什么權限,可以訪問什么資源
安全防護,跨站腳本攻擊,session
攻擊等
非常容易結合Spring
進行使用
3、Spring Security
與Shiro
的區(qū)別
優(yōu)點:
1、Spring Security基于Spring開發(fā),項目如果使用Spring作為基礎,配合Spring Security做權限更加方便。而Shiro需要和Spring進行整合開發(fā)
2、Spring Security功能比Shiro更加豐富,例如安全防護方面
3、Spring Security社區(qū)資源相對比Shiro更加豐富
缺點:
1)Shiro的配置和使用比較簡單,Spring Security上手復雜些
2)Shiro依賴性低,不需要依賴任何框架和容器,可以獨立運行。Spring Security依賴Spring容器
需要實現(xiàn)的過濾器和處理器
1、LogoutSuccessHandler:
表示登出處理器
2、驗證碼過濾器Filter
3、登錄認證成功、失敗處理器
4、BasicAuthenticationFilter:
該過濾器用于普通http請求進行身份認證
5、AuthenticationEntryPoint:
表示認證失敗處理器
6、AccessDenieHandler:
用戶發(fā)起無權限訪問請求的處理器
7、UserServiceDatils 接口:
該接口十分重要,用于從數(shù)據(jù)庫中驗證用戶名密碼
8、PasswordEncoder密碼驗證器
整合
1.添加相應依賴
<!-- springboot security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- jwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>com.github.axet</groupId> <artifactId>kaptcha</artifactId> <version>0.0.9</version> </dependency> <!-- hutool工具類--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.3</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.11</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.15</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
2.寫一個JWT工具類(生成JWT、解析JWT、判斷JWT是否過期)
import io.jsonwebtoken.*; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Date; @Data @Component @ConfigurationProperties(prefix = "hang.jwt") public class JwtUtils { //使用@ConfigurationProperties注解可以讀取配置文件中的信息,只要在 Bean 上添加上了這個注解,指定好配置文件中的前綴,那么對應的配置文件數(shù)據(jù)就會自動填充到 Bean 的屬性中 private long expire; private String secret; private String header; // 生成JWT public String generateToken(String username) { Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + 1000 * expire); return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(username) .setIssuedAt(nowDate) .setExpiration(expireDate) // 7天過期 .signWith(SignatureAlgorithm.HS512, secret) .compact(); } // 解析JWT public Claims getClaimsByToken(String jwt) { try { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(jwt) .getBody(); } catch (Exception e) { return null; } } // 判斷JWT是否過期 public boolean isTokenExpired(Claims claims) { return claims.getExpiration().before(new Date()); } }
#JWT配置 hang: jwt: header: Authorization expire: 604800 # 7天,s為單位 secret: abcdefghabcdefghabcdefghabcdefgh
封裝Result
import lombok.Data; import java.io.Serializable; @Data public class Result implements Serializable { private int code; private String msg; private Object data; public static Result succ(Object data) { return succ(200, "操作成功", data); } public static Result fail(String msg) { return fail(400, msg, null); } public static Result succ (int code, String msg, Object data) { Result result = new Result(); result.setCode(code); result.setMsg(msg); result.setData(data); return result; } public static Result fail (int code, String msg, Object data) { Result result = new Result(); result.setCode(code); result.setMsg(msg); result.setData(data); return result; } }
LoginSuccessHandler(登錄成功處理器)實現(xiàn)AuthenticationSuccessHandler
@Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Autowired JwtUtils jwtUtils; @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=UTF-8"); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); // 生成JWT,并放置到請求頭中 String jwt = jwtUtils.generateToken(authentication.getName()); httpServletResponse.setHeader(jwtUtils.getHeader(), jwt); Result result = Result.succ("SuccessLogin"); outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8)); outputStream.flush(); outputStream.close(); } }
LoginFailureHandler(登錄失敗處理器)實現(xiàn)AuthenticationFailureHandler
@Component public class LoginFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=UTF-8"); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); String errorMessage = "用戶名或密碼錯誤"; Result result; if (e instanceof CaptchaException) { errorMessage = "驗證碼錯誤"; result = Result.fail(errorMessage); } else { result = Result.fail(errorMessage); } outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8)); outputStream.flush(); outputStream.close(); } }
自定義的驗證碼異常
public class CaptchaException extends AuthenticationException { public CaptchaException(String msg) { super(msg); } }
驗證碼工具類
@Configuration public class KaptchaConfig { @Bean DefaultKaptcha producer() { Properties properties = new Properties(); properties.put("kaptcha.border", "no"); properties.put("kaptcha.textproducer.font.color", "black"); properties.put("kaptcha.textproducer.char.space", "4"); properties.put("kaptcha.image.height", "40"); properties.put("kaptcha.image.width", "120"); properties.put("kaptcha.textproducer.font.size", "30"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
驗證碼 Controller
@Autowired Producer producer; @GetMapping("/captcha") public Result Captcha() throws IOException { String key = UUID.randomUUID().toString(); String code = producer.createText(); BufferedImage image = producer.createImage(code); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(image, "jpg", outputStream); BASE64Encoder encoder = new BASE64Encoder(); String str = "data:image/jpeg;base64,"; String base64Img = str + encoder.encode(outputStream.toByteArray()); //隨機碼為key,驗證碼為value redisUtil.hset(Const.CAPTCHA_KEY, key, code, 120); return Result.succ( MapUtil.builder() .put("userKey", key) .put("captcherImg", base64Img) .build() ); }
驗證碼過濾器CaptchaFilter
@Component public class CaptchaFilter extends OncePerRequestFilter { @Autowired RedisUtil redisUtil; @Autowired LoginFailureHandler loginFailureHandler; //自定義處理邏輯 @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { String url = httpServletRequest.getRequestURI(); if ("/login".equals(url) && httpServletRequest.getMethod().equals("POST")) { // 校驗驗證碼 try { validate(httpServletRequest); } catch (CaptchaException e) { // 交給認證失敗處理器 loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); } } filterChain.doFilter(httpServletRequest, httpServletResponse); } // 校驗驗證碼邏輯 private void validate(HttpServletRequest httpServletRequest) { String code = httpServletRequest.getParameter("code"); String key = httpServletRequest.getParameter("userKey"); if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) { throw new CaptchaException("驗證碼錯誤"); } if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY, key))) { throw new CaptchaException("驗證碼錯誤"); } // 若驗證碼正確,執(zhí)行以下語句 // 一次性使用 redisUtil.hdel(Const.CAPTCHA_KEY, key); } }
JWT過濾器JwtAuthenticationFilter
檢驗JWT是否正確以及是否過期
public class JwtAuthenticationFilter extends BasicAuthenticationFilter { @Autowired JwtUtils jwtUtils; @Autowired UserDetailServiceImpl userDetailService; @Autowired SysUserService sysUserService; public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String jwt = request.getHeader(jwtUtils.getHeader()); // 這里如果沒有jwt,繼續(xù)往后走,因為后面還有鑒權管理器等去判斷是否擁有身份憑證,所以是可以放行的 // 沒有jwt相當于匿名訪問,若有一些接口是需要權限的,則不能訪問這些接口 if (StrUtil.isBlankOrUndefined(jwt)) { chain.doFilter(request, response); return; } Claims claim = jwtUtils.getClaimsByToken(jwt); if (claim == null) { throw new JwtException("token 異常"); } if (jwtUtils.isTokenExpired(claim)) { throw new JwtException("token 已過期"); } String username = claim.getSubject(); // 獲取用戶的權限等信息 SysUser sysUser = sysUserService.getByUsername(username); // 構建UsernamePasswordAuthenticationToken,這里密碼為null,是因為提供了正確的JWT,實現(xiàn)自動登錄 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getId())); SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(request, response); } }
SecurityContextHolder.getContext().getAuthentication().getPrincipal()等方法獲取到當前登錄的用戶信息
JWT認證失敗處理器JwtAuthenticationEntryPoint
@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { //認證失敗的處理 @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=UTF-8"); httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); Result result = Result.fail("請先登錄"); outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8)); outputStream.flush(); outputStream.close(); } }
從數(shù)據(jù)庫中驗證用戶名、密碼:UserServiceDetails、AuthenticationManager、UserDetails
SpringSecurity中的認證管理器AuthenticationManager是一個抽象接口,用以提供各種認證方式。一般我們都使用從數(shù)據(jù)庫中驗證用戶名、密碼是否正確這種認證方式。
AuthenticationManager的默認實現(xiàn)類是ProviderManager,ProviderManager提供很多認證方式,DaoAuthenticationProvider是AuthenticationProvider的一種實現(xiàn),可以通過實現(xiàn)UserDetailsService接口的方式來實現(xiàn)數(shù)據(jù)庫查詢方式登錄。
Spring Security在拿到UserDetails之后,會去對比Authentication(Authentication如何得到?我們使用的是默認的UsernamePasswordAuthenticationFilter,它會讀取表單中的用戶信息并生成Authentication),若密碼正確,則Spring Secuity自動幫忙完成登錄
定義一個UserDetails接口的實現(xiàn)類,稱為AccountUser實現(xiàn)所有方法
public interface UserDetails extends Serializable { //獲取用戶權限 Collection<? extends GrantedAuthority> getAuthorities(); //用戶密碼 String getPassword(); //用戶名 String getUsername(); //用戶是否過期 boolean isAccountNonExpired(); //用戶是否被鎖定 boolean isAccountNonLocked(); //認證信息是否過期 boolean isCredentialsNonExpired(); //用戶啟用還是禁用 boolean isEnabled(); }
實現(xiàn) UserDetails (默認有權限管理功能)
public class AccountUser implements UserDetails { private Long userId; private static final long serialVersionUID = 540L; private static final Log logger = LogFactory.getLog(User.class); private String password; private final String username; private final Collection<? extends GrantedAuthority> authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; public AccountUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) { this(userId, username, password, true, true, true, true, authorities); } public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor"); this.userId = userId; this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isAccountNonExpired() { return this.accountNonExpired; } @Override public boolean isAccountNonLocked() { return this.accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return this.credentialsNonExpired; } @Override public boolean isEnabled() { return this.enabled; } }
實現(xiàn) UserDetailsService 重寫其loadUserByUsername方法 使用用戶名在數(shù)據(jù)庫中查找用戶信息返回
@Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired SysUserService sysUserService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserService.getByUsername(username); if (sysUser == null) { throw new UsernameNotFoundException("用戶名或密碼錯誤"); } return new AccountUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), getUserAuthority(sysUser.getId())); } /** * 獲取用戶權限信息(角色、菜單權限) * @param userId * @return */ public List<GrantedAuthority> getUserAuthority(Long userId) { // 實際怎么寫以數(shù)據(jù)表結構為準,這里只是寫個例子 // 角色(比如ROLE_admin),菜單操作權限(比如sys:user:list) String authority = sysUserService.getUserAuthorityInfo(userId); // 比如ROLE_admin,ROLE_normal,sys:user:list,... return AuthorityUtils.commaSeparatedStringToAuthorityList(authority); } }
實現(xiàn)了上述幾個接口,從數(shù)據(jù)庫中驗證用戶名、密碼的過程將由框架幫我們完成,封裝隱藏了
無權限訪問的處理:AccessDenieHandler
當權限不足時,我們需要設置權限不足狀態(tài)碼403,并將錯誤信息返回給前端
@Component public class JwtAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=UTF-8"); httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); Result result = Result.fail(e.getMessage()); outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8)); outputStream.flush(); outputStream.close(); } }
登出處理器 LogoutSuccessHandler
1.將原來的 JWT 置為空返給前端
2.用空字符串覆蓋之前的 JWT (JWT是無狀態(tài) 無法銷毀 只能等過期 所以采用置空瀏覽器中保存的JWT)
3.清除SecurityContext中的用戶信息 (通過創(chuàng)建SecurityContextLogoutHandler對象,調用它的logout方法)
實現(xiàn) LogoutSuccessHandler 重寫 onLogoutSuccess 方法
@Component public class JWTLogoutSuccessHandler implements LogoutSuccessHandler { @Autowired JwtUtils jwtUtils; @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { if (authentication != null) { new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication); } httpServletResponse.setContentType("application/json;charset=UTF-8"); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); httpServletResponse.setHeader(jwtUtils.getHeader(), ""); Result result = Result.succ("SuccessLogout"); outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8)); outputStream.flush(); outputStream.close(); } }
密碼加密解密:PasswordEncoder
1.首先前端對密碼進行esa加密
2.后端對前端傳輸過來的密碼進行解密
3.再根據(jù)數(shù)據(jù)庫的加密規(guī)則BCrypt進行加密
SpringSecurity提供了用于密碼加密解密的工具類BCryptPasswordEncoder
需自定義PasswordEncoder類,并使其繼承BCryptPasswordEncoder,重寫其matches方法
@NoArgsConstructor public class PasswordEncoder extends BCryptPasswordEncoder { //判斷從前端接收的密碼與數(shù)據(jù)庫中的密碼是否一致 @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { // 接收到的前端的密碼 String pwd = rawPassword.toString(); // 進行rsa解密 try { pwd = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey, pwd); } catch (Exception e) { throw new BadCredentialsException(e.getMessage()); } if (encodedPassword != null && encodedPassword.length() != 0) { return BCrypt.checkpw(pwd, encodedPassword); } else { return false; } } }
Spring Security全局配置:SecurityConfig
需要繼承WebSecurityConfigurerAdapter(采用適配器模式,繼承后SecurityConfig可以看做是WebSecurityConfigurer)
SecurityConfig需要使用
@EnableGlobalMethodSecurity(prePostEnabled = true)
注解
Spring Security默認是禁用注解的,要想開啟注解,需要在繼承WebSecurityConfigurerAdapter的類上加@EnableGlobalMethodSecurity注解,來判斷用戶對某個控制層的方法是否具有訪問權限。prePostEnabled = true即可在方法前后進行權限檢查
Security內(nèi)置的權限注解如下:
@PreAuthorize:方法執(zhí)行前進行權限檢查@PreAuthorize("hasAuthority('sys:user:list')")
@PostAuthorize:方法執(zhí)行后進行權限檢查
@Secured:類似于 @PreAuthorize
可以在Controller的方法前添加這些注解表示接口需要什么權限。
配置類還需使用@EnableWebSecurity注解,該注解有兩個作用:1. 加載了WebSecurityConfiguration配置類, 配置安全認證策略。2.加載了AuthenticationConfiguration, 配置了認證信息。AuthenticationConfiguration這個類的作用就是用來創(chuàng)建ProviderManager。
@EnableWebSecurity完成的工作便是加載了WebSecurityConfiguration,AuthenticationConfiguration這兩個核心配置類,也就此將spring security的職責劃分為了配置安全信息,配置認證信息兩部分。
在SecurityConfig這個配置類中,我們需要將之前寫的攔截器和處理器都autowire進來,并使用@Bean注解,聲明JwtAuthenticationFilter和PasswordEncoder的構造函數(shù)。在JwtAuthenticationFilter的構造函數(shù)中,我們調用authenticationManager()方法給JwtAuthenticationFilter提供AuthenticationManager。
配置類需要重寫configure方法進行配置,該方法有多種重載形式,我們使用其中的兩種,其中一個用于配置url安全攔截配置,另一個用于AuthenticationManager配置UserDetailsService的實現(xiàn)類
@Configuration @EnableWebSecurity @RequiredArgsConstructor @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired LoginFailureHandler loginFailureHandler; @Autowired LoginSuccessHandler loginSuccessHandler; @Autowired CaptchaFilter captchaFilter; @Autowired JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired JwtAccessDeniedHandler jwtAccessDeniedHandler; @Autowired UserDetailServiceImpl userDetailService; @Autowired JWTLogoutSuccessHandler jwtLogoutSuccessHandler; @Bean JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager()); return jwtAuthenticationFilter; } private static final String[] URL_WHITELIST = { "/login", "/logout", "/captcha", "/favicon.ico" }; @Bean PasswordEncoder PasswordEncoder() { return new PasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() // 登錄配置 .formLogin() .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler) .and() .logout() .logoutSuccessHandler(jwtLogoutSuccessHandler) // 禁用session .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 配置攔截規(guī)則 .and() .authorizeRequests() .antMatchers(URL_WHITELIST).permitAll() .anyRequest().authenticated() // 異常處理器 .and() .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(jwtAccessDeniedHandler) // 配置自定義的過濾器 .and() .addFilter(jwtAuthenticationFilter()) // 驗證碼過濾器放在UsernamePassword過濾器之前 .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class) ; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService); } }
自定義權限校驗注解
Spring Security
提供了Spring EL
表達式,允許我們在定義接口訪問的方法上面添加注解,來控制訪問權限。
@PreAuthorize 注解用于配置接口要求用戶擁有某些權限才可訪問
方法 | 參數(shù) | 描述 |
---|---|---|
hasPermi | String | 驗證用戶是否具備某權限 |
lacksPermi | String | 驗證用戶是否不具備某權限,與 hasPermi邏輯相反 |
hasAnyPermi | String | 驗證用戶是否具有以下任意一個權限 |
hasRole | String | 判斷用戶是否擁有某個角色 |
lacksRole | String | 驗證用戶是否不具備某角色,與 isRole邏輯相反 |
hasAnyRoles | String | 驗證用戶是否具有以下任意一個角色,多個逗號分隔 |
使用 @ss
代表 PermissionService(許可服務) 類,對每個接口攔截并調用PermissionService
的對應方法判斷接口調用者的權限。
package com.example.framework.web.service; import java.util.Set; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import com.example.common.core.domain.entity.SysRole; import com.example.common.core.domain.model.LoginUser; import com.example.common.utils.SecurityUtils; import com.example.common.utils.StringUtils; import com.example.framework.security.context.PermissionContextHolder; /** * 自定義權限實現(xiàn),ss => SpringSecurity首字母 * 超級管理員擁有所有權限,不受權限約束。 */ @Service("ss") public class PermissionService { /** 所有權限標識 */ private static final String ALL_PERMISSION = "*:*:*"; /** 管理員角色權限標識 */ private static final String SUPER_ADMIN = "admin"; private static final String ROLE_DELIMETER = ","; private static final String PERMISSION_DELIMETER = ","; /** * 驗證用戶是否具備某權限 * * @param permission 權限字符串 * @return 用戶是否具備某權限 */ public boolean hasPermi(String permission) { if (StringUtils.isEmpty(permission)) { return false; } LoginUser loginUser = SecurityUtils.getLoginUser(); if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) { return false; } PermissionContextHolder.setContext(permission); return hasPermissions(loginUser.getPermissions(), permission); } /** * 驗證用戶是否不具備某權限,與 hasPermi邏輯相反 * * @param permission 權限字符串 * @return 用戶是否不具備某權限 */ public boolean lacksPermi(String permission) { return hasPermi(permission) != true; } /** * 驗證用戶是否具有以下任意一個權限 * * @param permissions 以 PERMISSION_NAMES_DELIMETER 為分隔符的權限列表 * @return 用戶是否具有以下任意一個權限 */ public boolean hasAnyPermi(String permissions) { if (StringUtils.isEmpty(permissions)) { return false; } LoginUser loginUser = SecurityUtils.getLoginUser(); if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) { return false; } PermissionContextHolder.setContext(permissions); Set<String> authorities = loginUser.getPermissions(); for (String permission : permissions.split(PERMISSION_DELIMETER)) { if (permission != null && hasPermissions(authorities, permission)) { return true; } } return false; } /** * 判斷用戶是否擁有某個角色 * * @param role 角色字符串 * @return 用戶是否具備某角色 */ public boolean hasRole(String role) { if (StringUtils.isEmpty(role)) { return false; } LoginUser loginUser = SecurityUtils.getLoginUser(); if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) { return false; } for (SysRole sysRole : loginUser.getUser().getRoles()) { String roleKey = sysRole.getRoleKey(); if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))) { return true; } } return false; } /** * 驗證用戶是否不具備某角色,與 isRole邏輯相反。 * * @param role 角色名稱 * @return 用戶是否不具備某角色 */ public boolean lacksRole(String role) { return hasRole(role) != true; } /** * 驗證用戶是否具有以下任意一個角色 * * @param roles 以 ROLE_NAMES_DELIMETER 為分隔符的角色列表 * @return 用戶是否具有以下任意一個角色 */ public boolean hasAnyRoles(String roles) { if (StringUtils.isEmpty(roles)) { return false; } LoginUser loginUser = SecurityUtils.getLoginUser(); if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) { return false; } for (String role : roles.split(ROLE_DELIMETER)) { if (hasRole(role)) { return true; } } return false; } /** * 判斷是否包含權限 * * @param permissions 權限列表 * @param permission 權限字符串 * @return 用戶是否具備某權限 */ private boolean hasPermissions(Set<String> permissions, String permission) { return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); } }
數(shù)據(jù)權限示例
// 符合system:user:list權限要求 @PreAuthorize("@ss.hasPermi('system:user:list')") // 不符合system:user:list權限要求 @PreAuthorize("@ss.lacksPermi('system:user:list')") // 符合system:user:add或system:user:edit權限要求即可 @PreAuthorize("@ss.hasAnyPermi('system:user:add,system:user:edit')")
角色權限示例
// 屬于user角色 @PreAuthorize("@ss.hasRole('user')") // 不屬于user角色 @PreAuthorize("@ss.lacksRole('user')") // 屬于user或者admin之一 @PreAuthorize("@ss.hasAnyRoles('user,admin')")
公開接口(不需要驗證權限可以公開訪問的)
使用注解方式,只需要在Controller
的類或方法上加入@Anonymous
該注解即可
// @PreAuthorize("@ss.xxxx('....')") 注釋或刪除掉原有的權限注解 @Anonymous @GetMapping("/list") public List<SysXxxx> list(SysXxxx xxxx) { return xxxxList; }
前端
前端需要做兩件事,一是登錄成功后把JWT存到localStore里面,二是在每次請求之前,都在請求頭中添加JWT
我們在store文件夾里創(chuàng)建index.js,將JWT定義為token,以及定義SET_TOKEN方法
Vue.use(Vuex) export default new Vuex.Store({ state: { token: '' }, mutations: { SET_TOKEN: (state, token) => { state.token = token localStorage.setItem("token", token) }, }, actions: { }, modules: { } })
在登錄成功時,接收后端傳來的JWT并保存
const jwt = res.headers['authorization'] this.$store.commit('SET_TOKEN', jwt)
在src文件夾下創(chuàng)建axios.js,進行axios配置,配置前置攔截器,為所有需要權限的請求裝配上header的token信息
const request = axios.create({ timeout: 5000, headers: { 'Content-Type': "application/json; charset=utf-8" } }) // 前置攔截,為所有需要權限的請求裝配上header的token信息 request.interceptors.request.use(config => { config.headers['Authorization'] = localStorage.getItem("token") return config })
到此這篇關于SpringBoot整合SpringSecurity和JWT的示例的文章就介紹到這了,更多相關Springboot SpringSecurity JWT內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springboot?+rabbitmq+redis實現(xiàn)秒殺示例
本文主要介紹了springboot?+rabbitmq+redis實現(xiàn)秒殺示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-07-07Java、JavaScript、Oracle、MySQL中實現(xiàn)的MD5加密算法分享
這篇文章主要介紹了Java、JavaScript、Oracle、MySQL中實現(xiàn)的MD5加密算法分享,需要的朋友可以參考下2014-09-09以用戶名注冊為例分析三種Action獲取數(shù)據(jù)的方式
這篇文章主要介紹了以用戶名注冊為例分析三種Action獲取數(shù)據(jù)的方式的相關資料,需要的朋友可以參考下2016-03-03