整合这个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:测试其它的接口
如果对你有帮助,或者对我感觉兴趣的话,可以关注我的公众号支持一下我噢