SpringBoot整合SpringSecurity和JWT的示例
JWT
1.介紹:
全稱 JSON Web Token,通過(guò)數(shù)字簽名的方式,以JSON為載體,在不同的服務(wù)終端之間安全的傳遞信息。
常用于授權(quán)認(rèn)證,用戶登錄后的每個(gè)請(qǐng)求都包含JWT,后端處理請(qǐng)求之前都要進(jìn)行校驗(yàn)。
2.組成:
Header:數(shù)據(jù)頭,令牌類型和加密算法
Payload:負(fù)載,請(qǐng)求體和其他數(shù)據(jù)
Signature:簽名,把頭部的base64UrlEncode與負(fù)載的base64UrlEncode拼接起來(lái)再進(jìn)行HMACSHA256加密
用戶認(rèn)證流程
1.用戶提交登錄表單(用戶名和密碼)
2.后端校驗(yàn)成功后生成JWT,通過(guò)response的header返回給前端
3.前端將
JWT保存到LocalStorage中4.之后所有的請(qǐng)求中請(qǐng)求頭都攜帶
JWT進(jìn)行身份認(rèn)證

Spring Security(安全框架)
1、介紹Spring Security是一個(gè)能夠?yàn)榛?code>Spring的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪問(wèn)控制解決方案的安全框架。
如果項(xiàng)目中需要進(jìn)行權(quán)限管理,具有多個(gè)角色和多種權(quán)限,我們可以使用Spring Security。
采用的是責(zé)任鏈的設(shè)計(jì)模式,是一堆過(guò)濾器鏈的組合,它有一條很長(zhǎng)的過(guò)濾器鏈。
2、功能Authentication (認(rèn)證),就是用戶登錄Authorization (授權(quán)),判斷用戶擁有什么權(quán)限,可以訪問(wèn)什么資源
安全防護(hù),跨站腳本攻擊,session攻擊等
非常容易結(jié)合Spring進(jìn)行使用
3、Spring Security與Shiro的區(qū)別
優(yōu)點(diǎn):
1、Spring Security基于Spring開(kāi)發(fā),項(xiàng)目如果使用Spring作為基礎(chǔ),配合Spring Security做權(quán)限更加方便。而Shiro需要和Spring進(jìn)行整合開(kāi)發(fā)
2、Spring Security功能比Shiro更加豐富,例如安全防護(hù)方面
3、Spring Security社區(qū)資源相對(duì)比Shiro更加豐富
缺點(diǎn):
1)Shiro的配置和使用比較簡(jiǎn)單,Spring Security上手復(fù)雜些
2)Shiro依賴性低,不需要依賴任何框架和容器,可以獨(dú)立運(yùn)行。Spring Security依賴Spring容器
需要實(shí)現(xiàn)的過(guò)濾器和處理器
1、LogoutSuccessHandler:
表示登出處理器
2、驗(yàn)證碼過(guò)濾器Filter
3、登錄認(rèn)證成功、失敗處理器
4、BasicAuthenticationFilter:
該過(guò)濾器用于普通http請(qǐng)求進(jìn)行身份認(rèn)證
5、AuthenticationEntryPoint:
表示認(rèn)證失敗處理器
6、AccessDenieHandler:
用戶發(fā)起無(wú)權(quán)限訪問(wèn)請(qǐng)求的處理器
7、UserServiceDatils 接口:
該接口十分重要,用于從數(shù)據(jù)庫(kù)中驗(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.寫一個(gè)JWT工具類(生成JWT、解析JWT、判斷JWT是否過(guò)期)
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 上添加上了這個(gè)注解,指定好配置文件中的前綴,那么對(duì)應(yīng)的配置文件數(shù)據(jù)就會(huì)自動(dòng)填充到 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天過(guò)期
.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是否過(guò)期
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,并放置到請(qǐng)求頭中
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 = "用戶名或密碼錯(cuò)誤";
Result result;
if (e instanceof CaptchaException) {
errorMessage = "驗(yàn)證碼錯(cuò)誤";
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)證碼過(guò)濾器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)證碼錯(cuò)誤");
}
if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY, key))) {
throw new CaptchaException("驗(yàn)證碼錯(cuò)誤");
}
// 若驗(yàn)證碼正確,執(zhí)行以下語(yǔ)句
// 一次性使用
redisUtil.hdel(Const.CAPTCHA_KEY, key);
}
}
JWT過(guò)濾器JwtAuthenticationFilter
檢驗(yàn)JWT是否正確以及是否過(guò)期
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());
// 這里如果沒(méi)有jwt,繼續(xù)往后走,因?yàn)楹竺孢€有鑒權(quán)管理器等去判斷是否擁有身份憑證,所以是可以放行的
// 沒(méi)有jwt相當(dāng)于匿名訪問(wèn),若有一些接口是需要權(quán)限的,則不能訪問(wè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 已過(guò)期");
}
String username = claim.getSubject();
// 獲取用戶的權(quán)限等信息
SysUser sysUser = sysUserService.getByUsername(username);
// 構(gòu)建UsernamePasswordAuthenticationToken,這里密碼為null,是因?yàn)樘峁┝苏_的JWT,實(shí)現(xiàn)自動(dòng)登錄
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("請(qǐng)先登錄");
outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
從數(shù)據(jù)庫(kù)中驗(yàn)證用戶名、密碼:UserServiceDetails、AuthenticationManager、UserDetails
SpringSecurity中的認(rèn)證管理器AuthenticationManager是一個(gè)抽象接口,用以提供各種認(rèn)證方式。一般我們都使用從數(shù)據(jù)庫(kù)中驗(yàn)證用戶名、密碼是否正確這種認(rèn)證方式。
AuthenticationManager的默認(rèn)實(shí)現(xiàn)類是ProviderManager,ProviderManager提供很多認(rèn)證方式,DaoAuthenticationProvider是AuthenticationProvider的一種實(shí)現(xiàn),可以通過(guò)實(shí)現(xiàn)UserDetailsService接口的方式來(lái)實(shí)現(xiàn)數(shù)據(jù)庫(kù)查詢方式登錄。
Spring Security在拿到UserDetails之后,會(huì)去對(duì)比Authentication(Authentication如何得到?我們使用的是默認(rèn)的UsernamePasswordAuthenticationFilter,它會(huì)讀取表單中的用戶信息并生成Authentication),若密碼正確,則Spring Secuity自動(dòng)幫忙完成登錄

定義一個(gè)UserDetails接口的實(shí)現(xiàn)類,稱為AccountUser實(shí)現(xiàn)所有方法
public interface UserDetails extends Serializable {
//獲取用戶權(quán)限
Collection<? extends GrantedAuthority> getAuthorities();
//用戶密碼
String getPassword();
//用戶名
String getUsername();
//用戶是否過(guò)期
boolean isAccountNonExpired();
//用戶是否被鎖定
boolean isAccountNonLocked();
//認(rèn)證信息是否過(guò)期
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ù)庫(kù)中查找用戶信息返回
@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("用戶名或密碼錯(cuò)誤");
}
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),這里只是寫個(gè)例子
// 角色(比如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)了上述幾個(gè)接口,從數(shù)據(jù)庫(kù)中驗(yàn)證用戶名、密碼的過(guò)程將由框架幫我們完成,封裝隱藏了
無(wú)權(quán)限訪問(wèn)的處理:AccessDenieHandler
當(dāng)權(quán)限不足時(shí),我們需要設(shè)置權(quán)限不足狀態(tài)碼403,并將錯(cuò)誤信息返回給前端
@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.將原來(lái)的 JWT 置為空返給前端
2.用空字符串覆蓋之前的 JWT (JWT是無(wú)狀態(tài) 無(wú)法銷毀 只能等過(guò)期 所以采用置空瀏覽器中保存的JWT)
3.清除SecurityContext中的用戶信息 (通過(guò)創(chuàng)建SecurityContextLogoutHandler對(duì)象,調(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.首先前端對(duì)密碼進(jìn)行esa加密
2.后端對(duì)前端傳輸過(guò)來(lái)的密碼進(jìn)行解密
3.再根據(jù)數(shù)據(jù)庫(kù)的加密規(guī)則BCrypt進(jìn)行加密
SpringSecurity提供了用于密碼加密解密的工具類BCryptPasswordEncoder
需自定義PasswordEncoder類,并使其繼承BCryptPasswordEncoder,重寫其matches方法
@NoArgsConstructor
public class PasswordEncoder extends BCryptPasswordEncoder {
//判斷從前端接收的密碼與數(shù)據(jù)庫(kù)中的密碼是否一致
@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)是禁用注解的,要想開(kāi)啟注解,需要在繼承WebSecurityConfigurerAdapter的類上加@EnableGlobalMethodSecurity注解,來(lái)判斷用戶對(duì)某個(gè)控制層的方法是否具有訪問(wèn)權(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注解,該注解有兩個(gè)作用:1. 加載了WebSecurityConfiguration配置類, 配置安全認(rèn)證策略。2.加載了AuthenticationConfiguration, 配置了認(rèn)證信息。AuthenticationConfiguration這個(gè)類的作用就是用來(lái)創(chuàng)建ProviderManager。
@EnableWebSecurity完成的工作便是加載了WebSecurityConfiguration,AuthenticationConfiguration這兩個(gè)核心配置類,也就此將spring security的職責(zé)劃分為了配置安全信息,配置認(rèn)證信息兩部分。
在SecurityConfig這個(gè)配置類中,我們需要將之前寫的攔截器和處理器都autowire進(jìn)來(lái),并使用@Bean注解,聲明JwtAuthenticationFilter和PasswordEncoder的構(gòu)造函數(shù)。在JwtAuthenticationFilter的構(gòu)造函數(shù)中,我們調(diào)用authenticationManager()方法給JwtAuthenticationFilter提供AuthenticationManager。
配置類需要重寫configure方法進(jìn)行配置,該方法有多種重載形式,我們使用其中的兩種,其中一個(gè)用于配置url安全攔截配置,另一個(gè)用于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)
// 配置自定義的過(guò)濾器
.and()
.addFilter(jwtAuthenticationFilter())
// 驗(yàn)證碼過(guò)濾器放在UsernamePassword過(guò)濾器之前
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService);
}
}
自定義權(quán)限校驗(yàn)注解
Spring Security提供了Spring EL表達(dá)式,允許我們?cè)诙x接口訪問(wèn)的方法上面添加注解,來(lái)控制訪問(wèn)權(quán)限。
@PreAuthorize 注解用于配置接口要求用戶擁有某些權(quán)限才可訪問(wèn)
| 方法 | 參數(shù) | 描述 |
|---|---|---|
| hasPermi | String | 驗(yàn)證用戶是否具備某權(quán)限 |
| lacksPermi | String | 驗(yàn)證用戶是否不具備某權(quán)限,與 hasPermi邏輯相反 |
| hasAnyPermi | String | 驗(yàn)證用戶是否具有以下任意一個(gè)權(quán)限 |
| hasRole | String | 判斷用戶是否擁有某個(gè)角色 |
| lacksRole | String | 驗(yàn)證用戶是否不具備某角色,與 isRole邏輯相反 |
| hasAnyRoles | String | 驗(yàn)證用戶是否具有以下任意一個(gè)角色,多個(gè)逗號(hào)分隔 |
使用 @ss 代表 PermissionService(許可服務(wù)) 類,對(duì)每個(gè)接口攔截并調(diào)用PermissionService的對(duì)應(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首字母
* 超級(jí)管理員擁有所有權(quán)限,不受權(quán)限約束。
*/
@Service("ss")
public class PermissionService
{
/** 所有權(quán)限標(biāo)識(shí) */
private static final String ALL_PERMISSION = "*:*:*";
/** 管理員角色權(quán)限標(biāo)識(shí) */
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)證用戶是否具有以下任意一個(gè)權(quán)限
*
* @param permissions 以 PERMISSION_NAMES_DELIMETER 為分隔符的權(quán)限列表
* @return 用戶是否具有以下任意一個(gè)權(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;
}
/**
* 判斷用戶是否擁有某個(gè)角色
*
* @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)證用戶是否具有以下任意一個(gè)角色
*
* @param roles 以 ROLE_NAMES_DELIMETER 為分隔符的角色列表
* @return 用戶是否具有以下任意一個(gè)角色
*/
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')")
公開(kāi)接口(不需要驗(yàn)證權(quán)限可以公開(kāi)訪問(wèn)的)
使用注解方式,只需要在Controller的類或方法上加入@Anonymous該注解即可
// @PreAuthorize("@ss.xxxx('....')") 注釋或刪除掉原有的權(quán)限注解
@Anonymous
@GetMapping("/list")
public List<SysXxxx> list(SysXxxx xxxx)
{
return xxxxList;
}
前端
前端需要做兩件事,一是登錄成功后把JWT存到localStore里面,二是在每次請(qǐng)求之前,都在請(qǐng)求頭中添加JWT
我們?cè)趕tore文件夾里創(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: {
}
})
在登錄成功時(shí),接收后端傳來(lái)的JWT并保存
const jwt = res.headers['authorization']
this.$store.commit('SET_TOKEN', jwt)
在src文件夾下創(chuàng)建axios.js,進(jìn)行axios配置,配置前置攔截器,為所有需要權(quán)限的請(qǐng)求裝配上header的token信息
const request = axios.create({
timeout: 5000,
headers: {
'Content-Type': "application/json; charset=utf-8"
}
})
// 前置攔截,為所有需要權(quán)限的請(qǐng)求裝配上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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的實(shí)現(xiàn)
- SpringBoot3.0+SpringSecurity6.0+JWT的實(shí)現(xiàn)
- Springboot WebFlux集成Spring Security實(shí)現(xiàn)JWT認(rèn)證的示例
- 詳解SpringBoot+SpringSecurity+jwt整合及初體驗(yàn)
- SpringBoot集成Spring security JWT實(shí)現(xiàn)接口權(quán)限認(rèn)證
- SpringBoot3.x接入Security6.x實(shí)現(xiàn)JWT認(rèn)證的完整步驟
- SpringBoot+SpringSecurity+jwt實(shí)現(xiàn)驗(yàn)證
- SpringBoot Security+JWT簡(jiǎn)單搭建的實(shí)現(xiàn)示例
相關(guān)文章
Jmeter參數(shù)化實(shí)現(xiàn)方法及應(yīng)用實(shí)例
這篇文章主要介紹了Jmeter參數(shù)化實(shí)現(xiàn)方法及應(yīng)用實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
Java操作Jenkins操作憑證(Credential)信息方式
這篇文章主要介紹了Java操作Jenkins操作憑證(Credential)信息方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-05-05
SpringBoot集成ShardingSphere實(shí)現(xiàn)數(shù)據(jù)庫(kù)分表
ShardingSphere?是一個(gè)開(kāi)源的分布式數(shù)據(jù)庫(kù)中間件,旨在為應(yīng)用提供數(shù)據(jù)庫(kù)分片、讀寫分離、分布式事務(wù)等功能,下面我們來(lái)看看SpringBoot如何集成ShardingSphere實(shí)現(xiàn)數(shù)據(jù)庫(kù)分表吧2024-12-12
java.lang.Runtime.exec的左膀右臂:流輸入和流讀取詳解
這篇文章主要介紹了java.lang.Runtime.exec的左膀右臂:流輸入和流讀取詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
Java Timezone類常見(jiàn)問(wèn)題_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java Timezone類常見(jiàn)問(wèn)題的相關(guān)資料,需要的朋友可以參考下2017-05-05
SpringBoot整合Mybatis,解決TypeAliases配置失敗的問(wèn)題
這篇文章主要介紹了SpringBoot整合Mybatis,解決TypeAliases配置失敗的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
IDEA 自帶的數(shù)據(jù)庫(kù)工具真的很牛(收藏版)
這篇文章主要介紹了IDEA 自帶的數(shù)據(jù)庫(kù)工具真的很牛(收藏版),本文以 IntelliJ IDEA/ Mac 版本作為演示,其他版本的應(yīng)該也差距不大,需要的朋友可以參考下2021-04-04

