整合這個SpringSecurity花了我好幾天的時間,也讓我很頭疼。
倒不是因爲它很難,只是我搜索到的前後端分離驗證,多多少少都有些問題。
下面我就把我完整的代碼貢獻出來、避免後面的人也走坑。
1、闡述幾個問題
這裏有幾個問題需要表達一下,當然你也可以直接跳到第二步開始。
1-1、什麼是SpringSecurity
它本質就是一個過濾器,然後在請求之前先執行這個過濾器,在這個過濾器裏面我們去進行用戶登錄,和權限進行判斷。
1-2、爲什麼我沒有用JWT來生成token
我覺得直接使用UUID生成Token就好了,沒必要使用JWT來生成一個又長又麻煩的Token。
1-3、下面的代碼是完整的代碼嘛?
可以說是了,本質上是基於Redis存儲數據驗證的,但是我覺得第一次學習Security的時候如果把全部都寫好加上去顯得很麻煩。Redis這塊可以根據自己定義去實現,我都標識出來了,很簡單。
1-4、如果我想獲取全部的代碼呢?
這個也不用擔心,我正在做這個前後端分離的,登錄和權限的框架,代碼是開源的。今天先分享怎麼去解決基於Security前後端分離,後面再寫一篇文章基於整個登錄權限的設計。
https://github.com/xdxTao/xdx-framework-SpringCloud
https://github.com/xdxTao/xdx-framework-vue
1-5、xxxxxx
感覺真是坑,很多關於Security的視頻,但是都沒有講到怎麼解決前後端分離的問題。昨天晚上搞到凌晨1點才弄完。
2、代碼部分
ps:token本應該隨機生成的,但是這裏只是演示,我就在yml裏面寫死了。其實這個很好理解的。
2-1、pom依賴
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
2-2、AjaxResult
ps:這個就是一個結果集統一封裝,相信大部分人都明白。
/**
* 封裝返回結果集
*
* @author 小道仙
* @date 2020年2月17日
*/
@Data
@Accessors(chain = true)
public class AjaxResult<T> {
/**
* 返回狀態碼
*/
private Integer code;
/**
* 返回的數據
*/
private T data;
/**
* 總條數
*/
private Integer total;
/**
* 成功與否
*/
private Boolean success;
/**
* 消息提示
*/
private String msg;
/**
* 錯誤描述
*/
private String errDesc;
/**
* 用戶token
*/
private String xdxToken;
public AjaxResult() {
}
/**
* 操作失敗
* @param errDesc 錯誤信息
*
* @author 小道仙
* @date 2020年2月17日
*/
public static AjaxResult<?> failure(String errDesc) {
return new AjaxResult<>().setErrDesc(errDesc).setSuccess(false);
}
/**
* 操作成功
* @param msg 返回消息
* @param total 總條數
* @param data 返回的數據
*
* @author 小道仙
* @date 2020年2月17日
*/
public static <T> AjaxResult<T> success(String msg,Integer total,T data){
AjaxResult<T> result = new AjaxResult<>();
result.setSuccess(true)
.setTotal(total)
.setMsg(msg);
return result;
}
/**
* 操作成功
* @param total 總條數
* @param data 返回的數據
*
* @author 小道仙
* @date 2020年2月17日
*/
public static <T> AjaxResult<T> success(T data,Integer total){
AjaxResult<T> result = new AjaxResult<>();
result.setSuccess(true)
.setTotal(total)
.setMsg("操作成功")
.setData(data);
return result;
}
/**
* 操作成功
* @param data 返回的數據
*
* @author 小道仙
* @date 2020年2月22日
*/
public static <T> AjaxResult<T> success(T data){
AjaxResult<T> result = new AjaxResult<>();
result.setSuccess(true)
.setMsg("操作成功")
.setData(data);
return result;
}
/**
* 操作成功
* @param msg 返回消息
*
* @author 小道仙
* @date 2020年2月17日
*/
public static <T> AjaxResult<T> success(String msg){
return success(msg,0,null);
}
/**
* 操作成功
* @param msg 返回消息
* @param total 總條數
*
* @author 小道仙
* @date 2020年2月17日
*/
public static <T> AjaxResult<T> success(String msg,Integer total){
return success(msg,total,null);
}
/**
* 操作成功
*
* @author 小道仙
* @date 2020年2月17日
*/
public static <T> AjaxResult<T> success(){
return success("操作成功",0,null);
}
}
2-3、SecurityConfig
2-3-1:SecurityConfig
import com.xdx97.framework.config.security.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 未登陸時返回 JSON 格式的數據給前端(否則爲 html)
*/
@Autowired
AjaxAuthenticationEntryPoint authenticationEntryPoint;
/**
* 註銷成功返回的 JSON 格式數據給前端(否則爲 登錄時的 html)
*/
@Autowired
AjaxLogoutSuccessHandler logoutSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 去掉 CSRF
http.csrf().disable()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and()
// 基於Token 不需要Session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 登錄處理
.and()
.formLogin()
.loginProcessingUrl("/user/login")
.permitAll()
// 登錄和權限控制
.and()
.authorizeRequests()
.anyRequest()
// RBAC 動態 url 認證
.access("@rbacauthorityservice.hasPermission(request,authentication)")
//註銷處理
.and()
.logout()//默認註銷行爲爲logout
.logoutUrl("/user/loginOut")
.logoutSuccessHandler(logoutSuccessHandler)
.permitAll();
}
}
2-3-2:AjaxAuthenticationEntryPoint
import com.alibaba.fastjson.JSON;
import com.xdx97.framework.common.AjaxResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 用戶沒有登錄時返回給前端的數據
*/
@Component
public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
AjaxResult ajaxResult = new AjaxResult();
String flagName = httpServletRequest.getAttribute("flagName").toString();
if (flagName.equals("未登錄")){
ajaxResult.setCode(888)
.setErrDesc("未登錄,請登錄!");
} else if (flagName.equals("權限不足")){
ajaxResult.setCode(999)
.setErrDesc("權限不足!");
} else{
ajaxResult.setCode(000)
.setErrDesc("系統異常!");
};
httpServletResponse.setContentType("text/html;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(ajaxResult));
}
}
2-3-2:AjaxLogoutSuccessHandler
import com.alibaba.fastjson.JSON;
import com.xdx97.framework.common.AjaxResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 退出登錄
*/
@Component
public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// 從這裏拿到token 然後把這個token註銷
String token = httpServletRequest.getHeader("xdxToken");
// 去redis刪除token
System.out.println("123123213");
AjaxResult ajaxResult = new AjaxResult();
ajaxResult.setCode(100)
.setErrDesc("退出成功!");
httpServletResponse.setContentType("text/html;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(ajaxResult));
}
}
2-3-2:RbacAuthorityService
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
@Component("rbacauthorityservice")
public class RbacAuthorityService {
@Autowired
private Environment env;
public boolean hasPermission(HttpServletRequest request,Authentication authentication) {
// 獲取當前請求的URI
String requestURI = request.getRequestURI();
// 放開登錄url
if (requestURI.equals("/user/login")){
return true;
}
// 登錄判斷
String token = request.getHeader("xdxToken");
if (token == null || !token.equals(env.getProperty("xdxToken"))){
request.setAttribute("flagName","未登錄");
return false;
}
// 權限判斷
// 利用token去Redis取出當前角色的權限,這裏就直接寫死了
List<String> roles = new ArrayList<>();
roles.add("/user/list");
roles.add("/user/menu");
roles.add("/user/loginOut");
if (!roles.contains(requestURI)){
request.setAttribute("flagName","權限不足");
return false;
}
return true;
}
}
2-4:另外我們需要三個接口,一個登錄(/user/login),一個測試(/user/list),一個測試(/authority/menu/list)
ps:兩個測試接口你不必在意裏面的實現,直接打印一句話就也行了,主要是看能不能訪問到。
2-4-1:登錄(/user/login)
controller
@GetMapping("/user/login")
public AjaxResult<?> login(@RequestParam String userName, @RequestParam String userPassword){
User user = new User();
user.setUserName(userName).setUserPassword(userPassword);
return userServiceImpl.login(user);
}
service:這個Environment 是用來獲取yml文件裏面的值的
@Autowired
private Environment env;
@Override
public AjaxResult<?> login(User user) {
AjaxResult ajaxResult = new AjaxResult();
/**
* 1、獲取到了用戶名和密碼去進行判斷是否正確
* 2、如果驗證不成功,這裏我默認用戶名密碼必須等於 admin admin
*/
if (! ("admin".equals(user.getUserName()) && "admin".equals(user.getUserPassword()))){
ajaxResult.setCode(222).setErrDesc("用戶名或密碼錯誤!");
return ajaxResult;
}
// 3、如果驗證成功了,就返回 token,當然了我們現需要把token存入Redis這裏就省略了
ajaxResult.setCode(200).setMsg("登錄成功!").setXdxToken(env.getProperty("xdxToken"));
return ajaxResult;
}
2-5、yml
2-6:總結
RbacAuthorityService是對所有請求進行攔截的,當被攔截到時,就會進入AjaxAuthenticationEntryPoint
當我們要退出的時候訪問 (/user/loginOut) 這時會進入 AjaxLogoutSuccessHandler
當然了你可以根據自己的需求繼續加入Filter
3、演示
3-1:登錄/退出
ps:我也不知道爲啥登錄只能用get請求,還沒研究爲什麼,有興趣可以自行研究。
3-2:測試其它的接口
如果對你有幫助,或者對我感覺興趣的話,可以關注我的公衆號支持一下我噢