SpringSecurity +JWT 實現前后端分離的登錄
要實現前后端分離,需要考慮以下2個問題:
-
項目不再基於session了,如何知道訪問者是誰?
-
如何確認訪問者的權限?
前后端分離,一般都是通過token實現,本項目也是一樣;用戶登錄時,生成token及token過期時間,token與用戶是一一對應關系,調用接口的時候,把token放到header或請求參數中,服務端就知道是誰在調用接口,登錄如下所示:
SpringSecurity登錄認證流程:https://blog.csdn.net/weixin_44588495/article/details/105907312
1.搭建步驟
1. 引入依賴
<!-- springboot security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</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>
<!--hutools工具類-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.3</version>
</dependency>
2.application.properties配置文件
#spring security
spring.security.user.password=123456
spring.security.user.name=admin
#jwt
jwt.header=Authorization
#過期時間設置為7天
jwt.expire=7
#32位的字符
jwt.secret=ji8n3439n439n43ld9ne9343fdfer49h
這里使用配置文件的方式配置用戶名和密碼,比較簡單便於測試,實際項目中是從數據庫取用戶數據
3.創建JWT工具類
該工具類主要包括3個方法,jwt的token的生成和解析以及token的過期與否
@Component
@Data
@ConfigurationProperties(prefix = "jwt")
public class JwtUtil {
private int expire;
private String secret;
private String header;
/**
* 生成jwt
* @param userName
* @return
*/
public String generateToken(String userName)
{
Date nowDate = new Date();
Date expireDate = DateUtil.offsetDay(nowDate,expire);//設置過期時間
return Jwts.builder()
.setHeaderParam("","")
.setSubject(userName)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
}
/**
* 解析JWT
* @param jwt
* @return
*/
public Claims parseClaim(String jwt)
{
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(jwt).getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
*jwt是否過期
*
*/
public boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
}
便於后續的配置,這里成員變量的值都寫到配置文件中
4.創建SecurityConfig配置文件
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private LoginFailureHandler loginFailureHandler;
@Resource
private LoginSuccessHandler loginSuccessHandler;
@Resource
private CaptchaFilter captchaFilter;
/* @Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.formLogin()
.successHandler(loginSuccessHandler)//登錄成功后的處理,會生成jwt的token並返回
.failureHandler(loginFailureHandler)//登錄失敗后的處理
.and()//關閉session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()//配置安全訪問規則
.authorizeRequests()
.antMatchers("/login","/captcha","/logout","/favicon.ico").permitAll()
.anyRequest().authenticated()
//添加自定義驗證碼過濾器,在UsernamePasswordAuthenticationFilter之前
.and()
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
}
5.登錄成功和失敗處理類
-
LoginSuccessHandler
LoginSuccessHandler類實現接口AuthenticationSuccessHandler,並且重寫onAuthenticationSuccess方法,在方法中實現對登錄成功的邏輯處理,生成lwt並且將jwt放在請求頭中返回給前端
@Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Autowired private JwtUtil jwtUtil; @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=UTF-8"); ResponseResult responseResult = ResponseResult.createBySuccessMessage("登錄成功"); //生成jwt String jwt = jwtUtil.generateToken(authentication.getName()); //把生成的jwt放在請求頭中返回,前端以后訪問后端接口請求頭都需要帶上它 httpServletResponse.setHeader(jwtUtil.getHeader(),jwt); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); outputStream.write(JSONUtil.toJsonStr(responseResult).getBytes("UTF-8")); outputStream.flush(); outputStream.close(); } }
-
LoginFailureHandler
LoginFailureHandler實現接口AuthenticationFailureHandler並且實現onAuthenticationFailure方法,在方法中實現對登錄失敗的邏輯處理。
@Slf4j @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"); ResponseResult responseResult =ResponseResult.createByErrorMessage(e.getMessage()); ServletOutputStream outputStream = httpServletResponse.getOutputStream(); outputStream.write(JSONUtil.toJsonStr(responseResult).getBytes("UTF-8")); outputStream.flush(); outputStream.close(); } }
2.認證失敗異常處理
由於我們的securityConfig的配置文件中只是對部分的url放行不做認證,其他的訪問請求都需要做認證。比如我們現在訪問localhost:8081/sys/menu/nav這個請求,認證失敗后返回的結果是給我們跳轉到內置的一個登錄頁面,因為這是一個前后端分離的,這不是我們希望的結果,我們希望的結果是返回一個統一格式的返回結果給前端。
這里需要配置我們的AuthenticationEntryPoint,從官方文檔解釋如下
因此我們需要創建一個類JwtAuthenticationEntryPoint類實現AuthenticationEntryPoint接口,並實現該方法commence
然后在配置文件securityConfig中,注入該配置
如此設置后訪問沒有認證的接口也就不會再跳轉到登陸頁面,而是給前端返回一個統一的json數據格式。
3.登陸測試
使用postman進行登陸測試,首先發送請求獲取驗證碼,后台生成驗證碼的同時,將該驗證碼存儲到redis中,並將redis存儲的驗證碼的key給返回,即token=key,此token非JWT生成的token, JWT生成的token是在登陸成功后才生成的。
獲取到驗證碼后,進行登陸接口測試。