SpringBoot整合SpringSecurity和JWT的示例
JWT
1.介紹:
全稱 JSON Web Token
,通過數(shù)字簽名的方式,以JSON
為載體,在不同的服務(wù)終端之間安全的傳遞信息。
常用于授權(quán)認(rèn)證,用戶登錄后的每個請求都包含JWT
,后端處理請求之前都要進(jìn)行校驗(yàn)。
2.組成:
Header
:數(shù)據(jù)頭,令牌類型和加密算法
Payload
:負(fù)載,請求體和其他數(shù)據(jù)
Signature
:簽名,把頭部的base64UrlEncode與負(fù)載的base64UrlEncode拼接起來再進(jìn)行HMACSHA256加密
用戶認(rèn)證流程
1.用戶提交登錄表單(用戶名和密碼)
2.后端校驗(yàn)成功后生成JWT,通過response的header返回給前端
3.前端將
JWT
保存到LocalStorage
中4.之后所有的請求中請求頭都攜帶
JWT
進(jìn)行身份認(rèn)證
Spring Security(安全框架)
1、介紹Spring Security
是一個能夠?yàn)榛?code>Spring的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪問控制解決方案的安全框架。
如果項(xiàng)目中需要進(jìn)行權(quán)限管理,具有多個角色和多種權(quán)限,我們可以使用Spring Security。
采用的是責(zé)任鏈的設(shè)計(jì)模式,是一堆過濾器鏈的組合,它有一條很長的過濾器鏈。
2、功能Authentication
(認(rèn)證),就是用戶登錄Authorization
(授權(quán)),判斷用戶擁有什么權(quán)限,可以訪問什么資源
安全防護(hù),跨站腳本攻擊,session
攻擊等
非常容易結(jié)合Spring
進(jìn)行使用
3、Spring Security
與Shiro
的區(qū)別
優(yōu)點(diǎn):
1、Spring Security基于Spring開發(fā),項(xiàng)目如果使用Spring作為基礎(chǔ),配合Spring Security做權(quán)限更加方便。而Shiro需要和Spring進(jìn)行整合開發(fā)
2、Spring Security功能比Shiro更加豐富,例如安全防護(hù)方面
3、Spring Security社區(qū)資源相對比Shiro更加豐富
缺點(diǎn):
1)Shiro的配置和使用比較簡單,Spring Security上手復(fù)雜些
2)Shiro依賴性低,不需要依賴任何框架和容器,可以獨(dú)立運(yùn)行。Spring Security依賴Spring容器
需要實(shí)現(xiàn)的過濾器和處理器
1、LogoutSuccessHandler:
表示登出處理器
2、驗(yàn)證碼過濾器Filter
3、登錄認(rèn)證成功、失敗處理器
4、BasicAuthenticationFilter:
該過濾器用于普通http請求進(jìn)行身份認(rèn)證
5、AuthenticationEntryPoint:
表示認(rèn)證失敗處理器
6、AccessDenieHandler:
用戶發(fā)起無權(quán)限訪問請求的處理器
7、UserServiceDatils 接口:
該接口十分重要,用于從數(shù)據(jù)庫中驗(yàn)證用戶名密碼
8、PasswordEncoder密碼驗(yàn)證器
整合
1.添加相應(yīng)依賴
<!-- 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 上添加上了這個注解,指定好配置文件中的前綴,那么對應(yīng)的配置文件數(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(登錄成功處理器)實(shí)現(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(登錄失敗處理器)實(shí)現(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 = "驗(yàn)證碼錯誤"; result = Result.fail(errorMessage); } else { result = Result.fail(errorMessage); } outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8)); outputStream.flush(); outputStream.close(); } }
自定義的驗(yàn)證碼異常
public class CaptchaException extends AuthenticationException { public CaptchaException(String msg) { super(msg); } }
驗(yàn)證碼工具類
@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; } }
驗(yàn)證碼 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()); //隨機(jī)碼為key,驗(yàn)證碼為value redisUtil.hset(Const.CAPTCHA_KEY, key, code, 120); return Result.succ( MapUtil.builder() .put("userKey", key) .put("captcherImg", base64Img) .build() ); }
驗(yàn)證碼過濾器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")) { // 校驗(yàn)驗(yàn)證碼 try { validate(httpServletRequest); } catch (CaptchaException e) { // 交給認(rèn)證失敗處理器 loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); } } filterChain.doFilter(httpServletRequest, httpServletResponse); } // 校驗(yàn)驗(yàn)證碼邏輯 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("驗(yàn)證碼錯誤"); } if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY, key))) { throw new CaptchaException("驗(yàn)證碼錯誤"); } // 若驗(yàn)證碼正確,執(zhí)行以下語句 // 一次性使用 redisUtil.hdel(Const.CAPTCHA_KEY, key); } }
JWT過濾器JwtAuthenticationFilter
檢驗(yàn)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ù)往后走,因?yàn)楹竺孢€有鑒權(quán)管理器等去判斷是否擁有身份憑證,所以是可以放行的 // 沒有jwt相當(dāng)于匿名訪問,若有一些接口是需要權(quán)限的,則不能訪問這些接口 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(); // 獲取用戶的權(quán)限等信息 SysUser sysUser = sysUserService.getByUsername(username); // 構(gòu)建UsernamePasswordAuthenticationToken,這里密碼為null,是因?yàn)樘峁┝苏_的JWT,實(shí)現(xiàn)自動登錄 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getId())); SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(request, response); } }
SecurityContextHolder.getContext().getAuthentication().getPrincipal()等方法獲取到當(dāng)前登錄的用戶信息
JWT認(rèn)證失敗處理器JwtAuthenticationEntryPoint
@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { //認(rèn)證失敗的處理 @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ù)庫中驗(yàn)證用戶名、密碼:UserServiceDetails、AuthenticationManager、UserDetails
SpringSecurity中的認(rèn)證管理器AuthenticationManager是一個抽象接口,用以提供各種認(rèn)證方式。一般我們都使用從數(shù)據(jù)庫中驗(yàn)證用戶名、密碼是否正確這種認(rèn)證方式。
AuthenticationManager的默認(rèn)實(shí)現(xiàn)類是ProviderManager,ProviderManager提供很多認(rèn)證方式,DaoAuthenticationProvider是AuthenticationProvider的一種實(shí)現(xiàn),可以通過實(shí)現(xiàn)UserDetailsService接口的方式來實(shí)現(xiàn)數(shù)據(jù)庫查詢方式登錄。
Spring Security在拿到UserDetails之后,會去對比Authentication(Authentication如何得到?我們使用的是默認(rèn)的UsernamePasswordAuthenticationFilter,它會讀取表單中的用戶信息并生成Authentication),若密碼正確,則Spring Secuity自動幫忙完成登錄
定義一個UserDetails接口的實(shí)現(xiàn)類,稱為AccountUser實(shí)現(xiàn)所有方法
public interface UserDetails extends Serializable { //獲取用戶權(quán)限 Collection<? extends GrantedAuthority> getAuthorities(); //用戶密碼 String getPassword(); //用戶名 String getUsername(); //用戶是否過期 boolean isAccountNonExpired(); //用戶是否被鎖定 boolean isAccountNonLocked(); //認(rèn)證信息是否過期 boolean isCredentialsNonExpired(); //用戶啟用還是禁用 boolean isEnabled(); }
實(shí)現(xiàn) UserDetails (默認(rèn)有權(quán)限管理功能)
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; } }
實(shí)現(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())); } /** * 獲取用戶權(quán)限信息(角色、菜單權(quán)限) * @param userId * @return */ public List<GrantedAuthority> getUserAuthority(Long userId) { // 實(shí)際怎么寫以數(shù)據(jù)表結(jié)構(gòu)為準(zhǔn),這里只是寫個例子 // 角色(比如ROLE_admin),菜單操作權(quán)限(比如sys:user:list) String authority = sysUserService.getUserAuthorityInfo(userId); // 比如ROLE_admin,ROLE_normal,sys:user:list,... return AuthorityUtils.commaSeparatedStringToAuthorityList(authority); } }
實(shí)現(xiàn)了上述幾個接口,從數(shù)據(jù)庫中驗(yàn)證用戶名、密碼的過程將由框架幫我們完成,封裝隱藏了
無權(quán)限訪問的處理:AccessDenieHandler
當(dāng)權(quán)限不足時,我們需要設(shè)置權(quán)限不足狀態(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對象,調(diào)用它的logout方法)
實(shí)現(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.首先前端對密碼進(jìn)行esa加密
2.后端對前端傳輸過來的密碼進(jìn)行解密
3.再根據(jù)數(shù)據(jù)庫的加密規(guī)則BCrypt進(jìn)行加密
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(); // 進(jìn)行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默認(rèn)是禁用注解的,要想開啟注解,需要在繼承WebSecurityConfigurerAdapter的類上加@EnableGlobalMethodSecurity注解,來判斷用戶對某個控制層的方法是否具有訪問權(quán)限。prePostEnabled = true即可在方法前后進(jìn)行權(quán)限檢查
Security內(nèi)置的權(quán)限注解如下:
@PreAuthorize:方法執(zhí)行前進(jìn)行權(quán)限檢查@PreAuthorize("hasAuthority('sys:user:list')")
@PostAuthorize:方法執(zhí)行后進(jìn)行權(quán)限檢查
@Secured:類似于 @PreAuthorize
可以在Controller的方法前添加這些注解表示接口需要什么權(quán)限。
配置類還需使用@EnableWebSecurity注解,該注解有兩個作用:1. 加載了WebSecurityConfiguration配置類, 配置安全認(rèn)證策略。2.加載了AuthenticationConfiguration, 配置了認(rèn)證信息。AuthenticationConfiguration這個類的作用就是用來創(chuàng)建ProviderManager。
@EnableWebSecurity完成的工作便是加載了WebSecurityConfiguration,AuthenticationConfiguration這兩個核心配置類,也就此將spring security的職責(zé)劃分為了配置安全信息,配置認(rèn)證信息兩部分。
在SecurityConfig這個配置類中,我們需要將之前寫的攔截器和處理器都autowire進(jìn)來,并使用@Bean注解,聲明JwtAuthenticationFilter和PasswordEncoder的構(gòu)造函數(shù)。在JwtAuthenticationFilter的構(gòu)造函數(shù)中,我們調(diào)用authenticationManager()方法給JwtAuthenticationFilter提供AuthenticationManager。
配置類需要重寫configure方法進(jìn)行配置,該方法有多種重載形式,我們使用其中的兩種,其中一個用于配置url安全攔截配置,另一個用于AuthenticationManager配置UserDetailsService的實(shí)現(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()) // 驗(yàn)證碼過濾器放在UsernamePassword過濾器之前 .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class) ; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService); } }
自定義權(quán)限校驗(yàn)注解
Spring Security
提供了Spring EL
表達(dá)式,允許我們在定義接口訪問的方法上面添加注解,來控制訪問權(quán)限。
@PreAuthorize 注解用于配置接口要求用戶擁有某些權(quán)限才可訪問
方法 | 參數(shù) | 描述 |
---|---|---|
hasPermi | String | 驗(yàn)證用戶是否具備某權(quán)限 |
lacksPermi | String | 驗(yàn)證用戶是否不具備某權(quán)限,與 hasPermi邏輯相反 |
hasAnyPermi | String | 驗(yàn)證用戶是否具有以下任意一個權(quán)限 |
hasRole | String | 判斷用戶是否擁有某個角色 |
lacksRole | String | 驗(yàn)證用戶是否不具備某角色,與 isRole邏輯相反 |
hasAnyRoles | String | 驗(yàn)證用戶是否具有以下任意一個角色,多個逗號分隔 |
使用 @ss
代表 PermissionService(許可服務(wù)) 類,對每個接口攔截并調(diào)用PermissionService
的對應(yīng)方法判斷接口調(diào)用者的權(quán)限。
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; /** * 自定義權(quán)限實(shí)現(xiàn),ss => SpringSecurity首字母 * 超級管理員擁有所有權(quán)限,不受權(quán)限約束。 */ @Service("ss") public class PermissionService { /** 所有權(quán)限標(biāo)識 */ private static final String ALL_PERMISSION = "*:*:*"; /** 管理員角色權(quán)限標(biāo)識 */ private static final String SUPER_ADMIN = "admin"; private static final String ROLE_DELIMETER = ","; private static final String PERMISSION_DELIMETER = ","; /** * 驗(yàn)證用戶是否具備某權(quán)限 * * @param permission 權(quán)限字符串 * @return 用戶是否具備某權(quán)限 */ 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); } /** * 驗(yàn)證用戶是否不具備某權(quán)限,與 hasPermi邏輯相反 * * @param permission 權(quán)限字符串 * @return 用戶是否不具備某權(quán)限 */ public boolean lacksPermi(String permission) { return hasPermi(permission) != true; } /** * 驗(yàn)證用戶是否具有以下任意一個權(quán)限 * * @param permissions 以 PERMISSION_NAMES_DELIMETER 為分隔符的權(quán)限列表 * @return 用戶是否具有以下任意一個權(quán)限 */ 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; } /** * 驗(yàn)證用戶是否不具備某角色,與 isRole邏輯相反。 * * @param role 角色名稱 * @return 用戶是否不具備某角色 */ public boolean lacksRole(String role) { return hasRole(role) != true; } /** * 驗(yàn)證用戶是否具有以下任意一個角色 * * @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; } /** * 判斷是否包含權(quán)限 * * @param permissions 權(quán)限列表 * @param permission 權(quán)限字符串 * @return 用戶是否具備某權(quán)限 */ private boolean hasPermissions(Set<String> permissions, String permission) { return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); } }
數(shù)據(jù)權(quán)限示例
// 符合system:user:list權(quán)限要求 @PreAuthorize("@ss.hasPermi('system:user:list')") // 不符合system:user:list權(quán)限要求 @PreAuthorize("@ss.lacksPermi('system:user:list')") // 符合system:user:add或system:user:edit權(quán)限要求即可 @PreAuthorize("@ss.hasAnyPermi('system:user:add,system:user:edit')")
角色權(quán)限示例
// 屬于user角色 @PreAuthorize("@ss.hasRole('user')") // 不屬于user角色 @PreAuthorize("@ss.lacksRole('user')") // 屬于user或者admin之一 @PreAuthorize("@ss.hasAnyRoles('user,admin')")
公開接口(不需要驗(yàn)證權(quán)限可以公開訪問的)
使用注解方式,只需要在Controller
的類或方法上加入@Anonymous
該注解即可
// @PreAuthorize("@ss.xxxx('....')") 注釋或刪除掉原有的權(quán)限注解 @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,進(jìn)行axios配置,配置前置攔截器,為所有需要權(quán)限的請求裝配上header的token信息
const request = axios.create({ timeout: 5000, headers: { 'Content-Type': "application/json; charset=utf-8" } }) // 前置攔截,為所有需要權(quán)限的請求裝配上header的token信息 request.interceptors.request.use(config => { config.headers['Authorization'] = localStorage.getItem("token") return config })
到此這篇關(guān)于SpringBoot整合SpringSecurity和JWT的示例的文章就介紹到這了,更多相關(guān)Springboot SpringSecurity JWT內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring中的Eureka服務(wù)過期詳細(xì)解析
這篇文章主要介紹了Spring中的Eureka服務(wù)過期詳細(xì)解析,如果有一些服務(wù)過期了,或者宕機(jī)了,就不會調(diào)用shutdown()方法,也不會去發(fā)送請求下線服務(wù)實(shí)例,需要的朋友可以參考下2023-11-11springboot?+rabbitmq+redis實(shí)現(xiàn)秒殺示例
本文主要介紹了springboot?+rabbitmq+redis實(shí)現(xiàn)秒殺示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07Java、JavaScript、Oracle、MySQL中實(shí)現(xiàn)的MD5加密算法分享
這篇文章主要介紹了Java、JavaScript、Oracle、MySQL中實(shí)現(xiàn)的MD5加密算法分享,需要的朋友可以參考下2014-09-09java中char類型轉(zhuǎn)換成int類型的2種方法
這篇文章主要給大家介紹了關(guān)于java中char類型轉(zhuǎn)換成int類型的2種方法,因?yàn)閖ava是一門強(qiáng)類型語言,所以在數(shù)據(jù)運(yùn)算中會存在類型轉(zhuǎn)換,需要的朋友可以參考下2023-07-07以用戶名注冊為例分析三種Action獲取數(shù)據(jù)的方式
這篇文章主要介紹了以用戶名注冊為例分析三種Action獲取數(shù)據(jù)的方式的相關(guān)資料,需要的朋友可以參考下2016-03-03