首先在了解前后端分离模式下使用SpringSecurity框架之前,我们需要先了解token和jwt(Json Web Token)技术

token 和 session 的区别?

由于http协议是无状态的协议,一次请求之后浏览器端无法保存服务器端携带过来的数据,所以为了解决这一问题,所以就有session 的出现来保存本次会话状态下的数据。(session底层的实现是基于cookie来实现的,每次请求通过携带 Jsessionid来获取Session域中的数据)
SESSION 是服务器通过 Key-Value 对来保存数据的一种机制,比如 APP 的登录状态可以用 SESSION 来保存。

TOKEN 翻译过来叫令牌,令牌是什么意思?可以拿现实中的令牌对比,现实中的令牌起到通行证的作用,而这在服务端也是一样的。我们在登录后,服务端使用 SESSION 保存我们的登录状态,并把 SESSION 的 Key 返回给客户端,那么这个 Key 就成为我们的令牌(TOKEN),我们以后再访问数据,就直接把这个 TOKEN 随着请求一起发送给服务端,这样服务端通过这个 TOKEN 在 SESSION 中查找数据,如果有就说明 TOKEN 有效(就像你去旅游,关口认可你的通行证),并取出你的登录数据,利用你的用户信息(保存在登录数据内)查出你想要的内容

session的缺点:

1、服务器压力增大通常session是存储在内存中的,每个用户通过认证之后都会将session数据保存在服务器的内存中,而当用户量增大时,服务器的压力增大。2、CSRF跨站伪造请求攻击session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。3、扩展性不强如果将来搭建了多个服务器,虽然每个服务器都执行的是同样的业务逻辑,但是session数据是保存在内存中的(不是共享的),用户第一次访问的是服务器1,当用户再次请求时可能访问的是另外一台服务器2,服务器2获取不到session信息,就判定用户没有登陆过。

token 的实现原理

token其实就是在服务器端生成的令牌,当用户发起登录请求之后”/login“,用户登录成功之后,服务器端会拿到该登录用户的 用户名 以及 密码,使用 JWT技术来生成 一个 token令牌(其实就是一串加密的字符串),并且响应给前端。前端将该 token 令牌保存到  sessionStorage 或者 localStorage 中,前端每一次请求的时候都会携带上该 token令牌,服务器端接收到请求判断 本次请求是否携带 token,来进行放行本次请求,起到一个 验名身份的效果。不用每次请求 都去 session 中判断是否有当前用户来验明自己的身份。token如果存储在硬盘上的话,就说明该令牌永久有效。

JWT 技术

JWT 其实就是用来生成 token 的技术,传入对应的用户信息,根据传入的用户信息来生成对应的token密钥 , jwt通常有三部分组成:头部信息(header) 消息体, 和签名头部信息:指定了该JWT使用的签名算法消息体:包含JWT中储存的信息签名:通过密钥跟算法生成签名

搞清楚了 token 和 jwt 之后 我们来具体的谈谈 前后端分离模式下 如何完成权限的验证。

SpringSecurity 其实本质上就是一堆的 过滤器链来实现的, 每次请求都会进入对应的过滤器来完成 认证和 授权等。

难点 ::: SpringSecurity在接受前端 发起的 登录请求时,无法接受 json格式的数据,只能接收到 form表单提交过来的数据,也就是 url?name=value&name=value这种数据格式。所以前端传递json格式的数据发起请求时,SpringSecurity无法接收所以我们要进行重写 对应的 用户名密码过滤器类。UsernamePasswordAuthenticationFilter , 搞清楚这一点之后,我们来研究以下SpringSecurity的整个执行流程。


springSecurity的 携带 token的 认证 流程

开始配置

(1) 引入对应的maven依赖

<dependencies><!-- jwt 相关依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.75</version></dependency><!--导入WebMVC的依赖,不要写版本号是因为spring底层已经默认配置了--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--导入mybatis的场景启动器--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version></dependency><!--引入Jackson--><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId></dependency><!--log4j启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j</artifactId><version>1.3.8.RELEASE</version></dependency><!--JDBC连接驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--引入driud启动器--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><!--解决生成的token存储 --><dependency><groupId>com.diffplug.guava</groupId><artifactId>guava-cache</artifactId><version>19.0.0</version></dependency></dependencies>

(2) 自定义的 UserDetails 实体类 来供 springSecurity 传递用户信息给 对应的登录成功的过滤器

package org.jcgl.service.auth;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import java.util.Collection;@Component
public class MyUserDetails implements UserDetails {private String username;private String password;private Integer state;private Collection<? extends GrantedAuthority> authorities;public MyUserDetails() {}public MyUserDetails(String username, String password, Integer state, Collection<? extends GrantedAuthority> authorities) {this.username = username;this.password = password;this.state = state;this.authorities = authorities;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}// 账户是否未过期@Overridepublic boolean isAccountNonExpired() {return true;}// 账户是否未被锁@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}public Integer getState() {return state;}public void setState(Integer state) {this.state = state;}@Overridepublic String toString() {return "JwtUser{" +"username='" + username + '\'' +", password='" + password + '\'' +", state=" + state +", authorities=" + authorities +'}';}}

(3)自定义对应的UserDetailsService 实现类 来进行 登录用户的具体认证 信息

package org.jcgl.service.auth;import org.jcgl.entity.UserEntity;
import org.jcgl.service.RoleService;
import org.jcgl.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;import java.util.ArrayList;
import java.util.List;@Component
public class UserDetailsServiceImpl implements UserDetailsService {@Autowired@Qualifier("userServiceImpl")private UserService userService;@Autowired@Qualifier("roleServiceImpl")private RoleService roleService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {System.out.println("自定义用户名密码过滤器验证成功,生成token进入自定义的UserDetailsService,前端传入的用户名为==============>" + username);// 根据传入的用户名查询出对应的用户UserEntity user = userService.getUserByUserName(username);System.out.println("根据用户名查询出用户信息为:" + user);if (user == null) {System.out.println("根据用户名查询用户为空");//用户密码错误抛出异常throw new UsernameNotFoundException(String.format("%s.这个用户不存在", username));} else {System.out.println("根据用户名查询用户 不 为空,开始根据用户名查询出对应的角色!!!");//用户名存在,根据用户名查找出对应角色,该方法默认为 null 还未具体实现!List<String> roles = roleService.getRolesByUserName(username);// 创建对应的角色集合用来存储该用户对应的角色List<SimpleGrantedAuthority> authorities = new ArrayList<>();for (String role : roles) {// 将查询相出的角色 封装到SimpleGrantedAuthority对象中// 并且存入 List<SimpleGrantedAuthority> 集合中authorities.add(new SimpleGrantedAuthority(role));}System.out.println("loadUserByUsername......user ===> " + user);//将查询出来的用户名和,对应的密码,状态码和权限存入 自定义的 UserDetails对象中并返回// 返回的数据 将被 成功之后的处理器调用 successHandlerreturn new MyUserDetails(user.getUsername(), user.getPassword(), user.getDelState(), authorities);}}
}

(3)自定义请求的前置拦截器,任何请求都会先进入该拦截器,判断本次请求是否携带 token 以及 处理 token是过期等问题。

package org.jcgl.security.myComponent;import org.jcgl.security.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 自定义的前置过滤器,用来判断本次请求是否携带token* 每次请求都会被 拦截 包括登录请求!!!!*/
@Component
public class MyOncePerRequestFilter extends OncePerRequestFilter {@Autowired@Qualifier("userDetailsServiceImpl")private UserDetailsService userDetailsService;@Autowired@Qualifier("jwtTokenUtil")private JwtTokenUtil jwtTokenUtil;// 请求头 = Authorization;private String header = "Authorization";@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {// 拿到本次请求的 请求头String headerToken = request.getHeader(header);System.out.println("进入自定义的 token 过滤器");System.out.println("本次请求携带的请求头为:" + headerToken);System.out.println("本次请求方式为:" + request.getMethod());if (!StringUtils.isEmpty(headerToken)) {// 如果本次请求头 不为 空,则说明该用户已经登录// 取出 postman 自带的 请求格式String token = headerToken.replace("Bearer", "").trim();System.out.println("token不为空 : token = " + token);// 拿到本次 请求携带的 token 之后判断该token是否过期// 比较好的解决方案 : 用户登录成功之后将 该用户生成的 token存进 redis//将数据库版本的token设置过期时间为15~30分钟//如果数据库中的token版本过期,重新刷新获取新的token//注意:刷新获得新token是在token过期时间内有效。//如果token本身的过期(1周),强制登录,生成新token。// 生成判断标记boolean check = false;try {// 判断该 token是否过期check = this.jwtTokenUtil.isTokenExpired(token);} catch (Exception e) {new Throwable("令牌已过期,请重新登录。" + e.getMessage());}if (!check) {// token 未过期 , 通过本次请求携带的 token 来获取 用户名String username = jwtTokenUtil.getUsernameFromToken(token);System.out.println("本次请求的用户名为: " + username);// 判断用户不为空,则说明缓存中有该用户,并且 security上下文中授权信息还是空的// 上下文为空则说明该用户还未登录,因为登录之后会将该用户的权限 存储到上下文中if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) {// 通过 用户名来获取 对应的用户信息UserDetails userDetails = userDetailsService.loadUserByUsername(username);System.out.println("本次请求的用户信息为: " + userDetails);// 验证本次令牌(token)是否有效boolean isOk = false;try {// 传入用户名和token,判断该token中的用户名和 请求的用户名是否相等// 并且 token没有过期,说明token有效isOk = jwtTokenUtil.validateToken(token, userDetails);} catch (Exception e) {new Throwable("验证token无效:" + e.getMessage());}if (isOk) {// token有效 , 将用户信息存入springSecurity自带的token对象中// 方便后续校验UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());// 将本次token存入 UserDetails 里面 供自定义的用户名和密码过滤器调用authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 将 authentication 存入 ThreadLocal,方便后续获取用户信息SecurityContextHolder.getContext().setAuthentication(authentication);}}}}chain.doFilter(request, response);}
}

(4)自定义 登录判断的 用户名和密码的过滤器,用来接收前端传来的 json格式的数据

package org.jcgl.security.myComponent;import com.fasterxml.jackson.databind.ObjectMapper;
import org.jcgl.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;/*** 自定义usernameAndPassword过滤器* 只有 /login 登录请求才会被 拦截,其余请求不会被该过滤器进行过滤!!!*/
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {@Autowired@Qualifier("userServiceImpl")private UserService userService;@Overridepublic Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)|| request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {ObjectMapper mapper = new ObjectMapper();UsernamePasswordAuthenticationToken authRequest = null;//取authenticationBeanMap<String, String> authenticationBean = null;//用try with resource,方便自动释放资源try (InputStream is = request.getInputStream()) {authenticationBean = mapper.readValue(is, Map.class);} catch (IOException e) {//将异常放到自定义的异常类中throw new MyAuthenticationException(e.getMessage());}try {if (!authenticationBean.isEmpty()) {//获得账号、密码String username = authenticationBean.get(SPRING_SECURITY_FORM_USERNAME_KEY);String password = authenticationBean.get(SPRING_SECURITY_FORM_PASSWORD_KEY);System.out.println("进入自定义用户名密码过滤器!!!");//可以验证账号、密码System.out.println("username = " + username);System.out.println("password = " + password);//检测账号、密码是否存在if (userService.checkLogin(username, password)) {System.out.println("用户名和密码正确开始封装token");//将账号、加密后密码装入UsernamePasswordAuthenticationToken中authRequest = new UsernamePasswordAuthenticationToken(username, password);// 封装完毕之后再进入 自定义的 UserDetailsService 对象中进行判断setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}System.out.println("用户名或者密码不正确!!!");}} catch (Exception e) {throw new MyAuthenticationException(e.getMessage());}return null;} else {return this.attemptAuthentication(request, response);}}
}

(5)自定义登录成功 之后的过滤器 AuthenticationSuccessHandler

package org.jcgl.security.myComponent;import org.jcgl.entity.MenuEntity;
import org.jcgl.exception.JsonResult;
import org.jcgl.security.util.JwtTokenUtil;
import org.jcgl.security.util.TokenCache;
import org.jcgl.service.MenuService;
import org.jcgl.util.JacksonUtil;
import org.jcgl.utl.TreeUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 用户认证成功之后的实现类,用来接收 UserDetailsService 传来的 UserDetails 实体类进行判断*/
@Component
public class UserAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Autowired@Qualifier("jwtTokenUtil")private JwtTokenUtil jwtTokenUtil;@Autowired@Qualifier("menuServiceImpl")private MenuService menuService;/*** @param request* @param response* @param authentication 回调传入的 用户名和密码的信息* @throws IOException* @throws ServletException*/@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {System.out.println("登录成功,获取用户的信息封装到map集合中");// 获取UserDetailsService 传来的 账号信息UserDetails userDetails = (UserDetails) authentication.getPrincipal();System.out.println("用户信息::" + userDetails);// 设置security的上下文的用户信息SecurityContextHolder.getContext().setAuthentication(authentication);// 用户信息获取到之后。来根据用户名去 缓存中去获取token,如果没有获取到则说明是第一次登录,或者缓存已经过期// 生成一个新的token,再存入缓存中去。 下次直接去缓存中取,不用每次都生成// 从缓存中取出token为//先去缓存中找看是否有该tokenString token = TokenCache.getTokenFromCache(userDetails.getUsername());System.out.println("从缓存中根据用户名取出token查看是否有该用户!");if (null == token) {// 没有该token,生成一个新的token// 使用 jwtTokenUtil 工具包来生成 tokentoken = jwtTokenUtil.generateToken(userDetails);// 将生成的 token存储到缓存中, key存 用户名,value存储 对应的token密钥!TokenCache.putTokenToCache(userDetails.getUsername(), token);System.out.println("用户第一次登录,缓存中没有该token,生成新的token并存入缓存中为::" + token);}// 根据登录成功的用户名来获取该用户对应的前端菜单List<MenuEntity> menus = menuService.getMenusByUserName(userDetails.getUsername());// 对取出的menus菜单进行排序 ,使用 TreeUtil工具类List<MenuEntity> menuEntities = TreeUtil.parseMenuTree(menus);// 将该 用户名、权限、生成的token、用户对应的菜单存入 map中 并返回给前端// 创建map集合来存储 该用户的数据 String做key Object 做 valueMap<String, Object> map = new HashMap<>();// 存入用户名map.put("username", userDetails.getUsername());// 存入权限map.put("auth", userDetails.getAuthorities());// 存入对应菜单map.put("menus", menuEntities);// 存入对应的 token密钥map.put("token", token);// 再次将该map集合 使用工具类进行封装,响应给前端//装入token,不使用 mybatisPlus// R<Map<String, Object>> data = R.ok(map);response.setCharacterEncoding("utf-8");response.setContentType("application/json;charset=utf-8");response.setHeader("Access-Control-Allow-Origin", "*");response.setHeader("Access-Control-Allow-Method", "POST,GET");// 用户认证成功之后,根据登录用户的角色查询出对应的菜单,来并随着用户一起响应给前端!!!System.out.println("封装完毕将所有封装的数据响应给前端!!!");// 响应给前端的必须是 JSON字符串,不能是 JsonResult对象,使用Jackson对象response.getWriter().write(JacksonUtil.getJson(JsonResult.success(map)));}
}

(6)自定义登录失败 的 过滤器 AuthenticationFailureHandler

package org.jcgl.security.myComponent;import org.jcgl.exception.JsonResult;
import org.jcgl.util.JacksonUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
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 UserAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {response.setCharacterEncoding("utf-8");response.setContentType("application/json;charset=utf-8");response.getWriter().write(JacksonUtil.getJson(JsonResult.failed("用户名或者密码错误!")));}
}

(7)自定义 动态权限设置的处理器,做到每次具体请求时(除了"/login、/logout") 都会进入该 处理器判断本次请求是具有权限 来执行该操作。

package org.jcgl.security.myComponent;import org.jcgl.entity.BackendAuthorityApi;
import org.jcgl.service.BackendApiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;import javax.servlet.http.HttpServletRequest;
import java.util.List;/*** 自定义动态的权限类,来进行拦截 本次的请求是否具有 该登录的用户是否具有 该角色对应的权限* 该类会自动注入到 springSecurity 的容器中,每次请求都会 被该过滤器 进行拦截 并且判断*/
@Component
public class DynamicPermission {// 该属性是 后端API管理的 服务处对象,主要作用是根据当前用户 获取该用户对应的角色// 通过角色 获取该角色 对应的具体的权限,来判断本次请求是否具有该权限。@Autowired@Qualifier("backendApiServiceImpl")private BackendApiService backendApiService;/*** 该方法是接收用户登录成功之后的  请求后端 之后 获取 用户携带的 token , 从token 中来获取* 该用户 对应的 角色 通过 角色 来获取 对应的 具体的权限。** @param request        本次请求的具体参数* @param authentication 在 WebSecurityConfigAdapter配合类中来 配置动态资源时 传入的数据* @return* @throws MyAccessDeniedException*/public boolean checkAuthority(HttpServletRequest request, Authentication authentication) throws MyAccessDeniedException {// 获取到该 用户的类型Object principal = authentication.getPrincipal();System.out.println("接收到请求  进入权限的认证 。。。。");System.out.println("当前的请求对应用户信息为:" + principal);// 判断 该验证信息是否是 UserDetails 类型if (principal instanceof UserDetails) {// 将获取的认证对象 强转为 UserDetails 类型UserDetails userDetails = (UserDetails) principal;// 拿到当前用户的用户名String username = userDetails.getUsername();System.out.println("本次请求对应的用户名  username = " + username);// 通过用户名 来获取 该用户对应的后端 API 接口 权限List<BackendAuthorityApi> apiUrlByUserName = backendApiService.getApiUrlByUserName(username);System.out.println("该用户名角色对应的后端API 为: " + apiUrlByUserName);AntPathMatcher antPathMatcher = new AntPathMatcher();//当前访问路径String requestURI = request.getRequestURI();System.out.println("本次请求的url:" + requestURI);//提交类型String urlMethod = request.getMethod();System.out.println("本次请求的类型为:  " + urlMethod);// 到此为止,取出了 当前用户名对应的后端的API接口权限,本次请求url 以及 本次请求方式// 开始遍历该角色对应的 后端API接口集合 , 查看本次请求是否在该 用户的权限范围内。boolean result = apiUrlByUserName.stream().anyMatch(item -> {// 使用antPathMatcher 对象判断本次请求的 url 是否在 集合中boolean hashAntPath = antPathMatcher.match(item.getBackendApiUrl(), requestURI);//判断请求方式是否和数据库中匹配(数据库存储:GET,POST,PUT,DELETE)String dbMethod = item.getBackendApiMethod();//处理null,万一数据库存值时有nulldbMethod = (dbMethod == null) ? "" : dbMethod;// 返回本次请求的方式 是否在API 对应的请求方式之内。如果不在则返回 -1// 因为有的url 可能包含多个请求方式int hasMethod = dbMethod.indexOf(urlMethod);System.out.println("本次请求url该用户是否具有权限====》 " + hashAntPath);System.out.println("本次请求的方法是否正确====》" + hasMethod);System.out.println("本次请求是否通过=====》" + (hashAntPath && hasMethod != -1));//两者都成立,返回真,否则返回假 , 请求return hashAntPath && (hasMethod != -1);});//返回if (result) {return result;} else {System.out.println("没有权限抛出异常!");throw new MyAccessDeniedException("您没有访问该API的权限!");}} else {System.out.println("不是UserDetails类型,抛出异常!");// 不是 UserDetails 类型 对象throw new MyAccessDeniedException("不是UserDetails类型!");}}}

(8)自定义权限不足时的异常类

package org.jcgl.security.myComponent;import org.springframework.security.access.AccessDeniedException;/*** 自定义权限不足时的异常 ,该异常 在进行判断本次请求是否 有对应API权限的时候使用!!!*/// 超级重点,自定义权限不足的类的时候,继承 AccessDeniedException的时候 要继承Spring框架带的异常类// 而不是 Java中的 权限不足的异常类!!!
public class MyAccessDeniedException extends AccessDeniedException {public MyAccessDeniedException(String msg) {super(msg);}
}

(9)自定义权限不足时 抛出 对应 异常之后 应该进入的过滤器AccessDeniedHandler

package org.jcgl.security.myComponent;import org.jcgl.exception.JsonResult;
import org.jcgl.util.JacksonUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
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 MyAccessDeniedHandler implements AccessDeniedHandler {/*** 响应认证失败数据给前端** @param request* @param response* @param e* @throws IOException* @throws ServletException*/@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {// 走到这一步表示本次请求的权限不足 封装失败的数据响应给前端!!!// 封装权限不足的异常信息response.setContentType("application/json;charset=utf-8");response.setCharacterEncoding("utf-8");System.out.println("本次请求权限不足,进入权限不足处理类!");// 返回异常信息给前端!!!response.getWriter().write(JacksonUtil.getJson(JsonResult.noPermission("权限不足:" + e.getMessage())));}
}

(10)自定义用户身份信息不足时的 异常类

package org.jcgl.security.myComponent;import org.springframework.security.core.AuthenticationException;/*** 自定义异常类,继承AuthenticationException* 在有throws AuthenticationException方法上捕获* 方式:throw new  MyAuthenticationException* 用户 请求 身份验证信息不足时发生的异常!!!*/
public class MyAuthenticationException extends AuthenticationException {public MyAuthenticationException(String msg) {super(msg);}
}

(11)自定义用户身份不足时,进入的过滤器

package org.jcgl.security.myComponent;import org.jcgl.exception.JsonResult;
import org.jcgl.util.JacksonUtil;
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;/**** AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常** AccessDeniedHandler 用来解决认证过的用户访问无权限资源时的异常** 自定义的 身份验证失败之后的处理器 , 和自定义的权限不足的 过滤器(MyAccessDeniedHandler) 功能一样*/
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {/*** 响应身份验证不足的 消息给前端* @param request* @param response* @param e* @throws IOException* @throws ServletException*/@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {// 响应身份验证不足的提示信息给前端response.setCharacterEncoding("utf-8");response.setContentType("application/json;charset=utf-8");// 响应数据给前端response.getWriter().write(JacksonUtil.getJson(JsonResult.noPermission("访问此资源需要完全身份验证: "+e.getMessage())));}
}

(12)自定义 退出时的过滤器 用来请求 SecurityContextHodler 中的对应的用户信息

package org.jcgl.security.myComponent;import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** 自定义退出登录的处理器,实现 LogoutHandler重写 该退出接口中的方法* 只要是 logout 的请求 springSecurity会自动走到该过滤器,就是类似于是 请求是* login 会自动进入 用户名和密码过滤器一样!!!** 实现 步骤,获取到本次请求登录的 请求头携带的 token,请求 上下文中的数据*/
@Component
public class MyLogoutHandler implements LogoutHandler {// 请求头private String header = "Authorization";@Overridepublic void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {String token = request.getHeader(header);// 根据本次请求的 请求头来获取本次请求携带的 tokenSystem.out.println("退出登录请求的token:"+ token);System.out.println("本次请求的方法为:"+request.getMethod());if (!StringUtils.isEmpty(token)) {//postMan测试时,自动假如的前缀,要去掉。String returnToken = token.replace("Bearer", "").trim();System.out.println("本次请求的authentication = " + authentication);// 清空容器SecurityContextHolder.clearContext();}}
}

(13)自定义退出成功之后响应给前端的过滤器

package org.jcgl.security.myComponent;import org.jcgl.exception.JsonResult;
import org.jcgl.util.JacksonUtil;
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;
import java.io.PrintWriter;/*** 自定义退出登录成功之后的 组件 实现 LogoutSuccessHandler 组件,重写方法* springSecurity执行流程。执行退出方法之后 会先执行 退出过滤器,完毕之后* 再执行 退出成功的过滤器 我们在退出成功的过滤器中响应JSON给前端。*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {//        UserDetails user = (UserDetails) authentication.getPrincipal();
//        String username = user.getUsername();System.out.println("退出成功。。。。。。");PrintWriter out = response.getWriter();response.setCharacterEncoding("utf-8");response.setContentType("application/json;charset=utf-8");out.write(JacksonUtil.getJson(JsonResult.success("退出成功!")));}
}

(14)配置一些工具类

package org.jcgl.security.util;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;@Component
public class BCryptPasswordEncoderUtil extends BCryptPasswordEncoder {/*** 对密码进行加密* @param rawPassword* @return*/@Overridepublic String encode(CharSequence rawPassword) {return super.encode(rawPassword);}/*** 对密码进行比对* @param rawPassword* @param encodedPassword* @return*/@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {return super.matches(rawPassword,encodedPassword);}}
package org.jcgl.security.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.jcgl.service.auth.MyUserDetails;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import java.util.Date;
import java.util.HashMap;
import java.util.Map;/*** JWT生成令牌、验证令牌、获取令牌*/
@Component
public class JwtTokenUtil {//私钥private static final String SECRET_KEY = "coding-study";// 过期时间 毫秒,设置默认1周的时间过期private  static final long EXPIRATION_TIME = 3600000L * 24*7;/*** 生成令牌** @param userDetails 用户* @return 令牌*/public String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>(2);// 将token 存入缓存claims.put(Claims.SUBJECT, userDetails.getUsername());claims.put(Claims.ISSUED_AT, new Date());// 返回生成的tokenreturn generateToken(claims);}/*** 从令牌中获取用户名** @param token 令牌* @return 用户名*/public String getUsernameFromToken(String token) {String username = null;try {Claims claims = getClaimsFromToken(token);System.out.println("claims = " + claims.toString());username = claims.getSubject();} catch (Exception e) {System.out.println("e = " + e.getMessage());}return username;}/*** 判断令牌是否过期** @param token 令牌* @return 是否过期*/public Boolean isTokenExpired(String token) throws  Exception{try {Claims claims = getClaimsFromToken(token);Date expiration = claims.getExpiration();return expiration.before(new Date());} catch (Exception e) {new Throwable(e);}return true;}/*** 刷新令牌** @param token 原令牌* @return 新令牌*/public String refreshToken(String token) {String refreshedToken;try {Claims claims = getClaimsFromToken(token);claims.put(Claims.ISSUED_AT, new Date());refreshedToken = generateToken(claims);} catch (Exception e) {refreshedToken = null;}return refreshedToken;}/*** 验证令牌** @param token       令牌* @param userDetails 用户* @return 是否有效*/public Boolean validateToken(String token, UserDetails userDetails) throws Exception {MyUserDetails user = (MyUserDetails) userDetails;String username = getUsernameFromToken(token);return (username.equals(user.getUsername()) && !isTokenExpired(token));}/*** 从数据声明生成令牌** @param claims 数据声明* @return 令牌*/private String generateToken(Map<String, Object> claims) {Date expirationDate = new Date(System.currentTimeMillis()+ EXPIRATION_TIME);return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET_KEY).compact();}/*** 从令牌中获取数据声明** @param token 令牌* @return 数据声明*/private Claims getClaimsFromToken(String token) throws Exception {Claims claims = null;try {claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();} catch (Exception e) {new Throwable(e);}return claims;}
}
package org.jcgl.security.util;import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;/***  缓存管理token*/
public class TokenCache {private static final String TOKEN_KEY = "token_";private static Cache<String,String> cache = CacheBuilder.newBuilder().build();/***  将生成的token存储到缓存中,key 存用户名,value存 token令牌!* @param token*/public static void putTokenToCache(String username,String token) {cache.put(TOKEN_KEY+username,token);}/*** 根据用户名,判断缓存中是否有该token!* @return*/public static String getTokenFromCache(String username){return cache.getIfPresent(TOKEN_KEY+username);}}

(15)配置 跨域请求

package org.jcgl.security;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;/*** 设置后端跨域 请求 使用 WebMvcConfigAdapter 配置类来设置跨域请求*/
@Configuration
public class AccessControlAllowOriginFilter extends WebMvcConfigurerAdapter {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("*").allowedMethods("PUT", "DELETE", "GET", "POST", "OPTIONS").allowedHeaders("*").exposedHeaders("access-control-allow-headers","access-control-allow-methods","access-control-allow-origin","access-control-max-age","X-Frame-Options").allowCredentials(true).maxAge(3600);}
}

(16)所有的自定义的过滤器配置完毕之后,配置SpringSecurity的核心配置类,将各个自定义过滤器进行 注入。

package org.jcgl.security;import org.jcgl.security.myComponent.DynamicPermission;
import org.jcgl.security.myComponent.MyUsernamePasswordAuthenticationFilter;
import org.jcgl.security.myComponent.UserAuthenticationFailureHandler;
import org.jcgl.security.myComponent.UserAuthenticationSuccessHandler;
import org.jcgl.security.util.BCryptPasswordEncoderUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.filter.OncePerRequestFilter;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {// 以下的属性 依赖注入按顺序进行注入// 自定义的拦截器请求之前的拦截器,用来判断 本次请求的 token@Autowired@Qualifier("myOncePerRequestFilter")private OncePerRequestFilter oncePerRequestFilter;@Autowired@Qualifier("dynamicPermission")private DynamicPermission dynamicPermission;// 进入自定义的 用户名和密码过滤器,为什么使用依赖注入而是使用 @Bean 注解,因为// 使用自定义的用户名和密码过滤器的时候需要 同时设置 自定义的 登录成功和失败的过滤器!!!// 进入 自定义的 userDetailsService 类来完成二次判断,并且将获取的数据封装到 UserDetails 实体类中// 并且响应给 成的处理器@Autowired@Qualifier("userDetailsServiceImpl")private UserDetailsService userDetailsService;//自定义登录成功过滤器@Autowiredprivate UserAuthenticationSuccessHandler myAuthenticationSuccessHandler;// 自定义登录失败的过滤器@Autowiredprivate UserAuthenticationFailureHandler myAuthenticationFailureHandler;// 成功和失败过滤器 只会走一个!!!// 自定义的权限不足的 过滤器@Autowired@Qualifier("myAccessDeniedHandler")private AccessDeniedHandler accessDeniedHandler;// 自定义的 身份验证 不足的过滤器@Autowired@Qualifier("myAuthenticationEntryPoint")private AuthenticationEntryPoint authenticationEntryPoint;// 自定义退出过滤器@Autowired@Qualifier("myLogoutHandler")private LogoutHandler logoutHandler;// 自定义退出成功过滤器@Autowired@Qualifier("myLogoutSuccessHandler")private LogoutSuccessHandler logoutSuccessHandler;// 自定义密码加密和匹配工具类@AutowiredBCryptPasswordEncoderUtil bCryptPasswordEncoderUtil;/*** 从容器中取出 AuthenticationManagerBuilder,执行方法里面的逻辑之后,放回容器** @param authenticationManagerBuilder* @throws Exception*/@Autowiredpublic void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoderUtil);}@Overrideprotected void configure(HttpSecurity http) throws Exception {//第1步:解决跨域问题。cors 预检请求放行,让Spring security 放行所有preflight request(cors 预检请求)http.authorizeRequests().requestMatchers(CorsUtils::isPreFlightRequest).permitAll();//第2步:让Security永远不会创建HttpSession,它不会使用HttpSession来获取SecurityContexthttp.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().headers().cacheControl();//第3步:请求权限配置//放行注册API请求,其它任何请求都必须经过身份验证.http.authorizeRequests().antMatchers(HttpMethod.POST, "/user/register").permitAll()//ROLE_ADMIN可以操作任何事情//.antMatchers("/**").hasRole("ADMIN")//同等上一行代码//.antMatchers("/**").hasAuthority("ROLE_ADMIN")/*由于使用动态资源配置,以上代码在数据库中配置如下:在sys_backend_api_table中添加一条记录backend_api_id=1,backend_api_name = 所有API,backend_api_url=/**,backend_api_method=GET,POST,PUT,DELETE*///动态加载资源 , 每次请求都会进入该方法来进行判断用户 是否具有权限来执行该操作!!!.anyRequest().access("@dynamicPermission.checkAuthority(request,authentication)");//第1步:登录,因为使用前端发送JSON方式进行登录,所以登录模式不设置也是可以的。http.formLogin();// 第2步,添加自定义的 拦截器 OncePerRequestFilter,在进入自定义的用户名和密码过滤器之前进行拦截http.addFilterBefore(oncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);//第3步:拦截账号、密码。覆盖 UsernamePasswordAuthenticationFilter过滤器http.addFilterAt(myUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);// 进入自定义过滤器判断之后,再进入 自定义的UserDetailsService 来进行二次判断// 如果用户请求 出现异常来进行处理//第6步:处理异常情况:认证失败和权限不足http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);// 第5步,自定义退出登录的过滤器http.logout().addLogoutHandler(logoutHandler).logoutSuccessHandler(logoutSuccessHandler);}/*** 手动注册账号、密码拦截器** @return* @throws Exception*/@BeanMyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter() throws Exception {MyUsernamePasswordAuthenticationFilter filter = new MyUsernamePasswordAuthenticationFilter();//修改过滤器//成功后处理filter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);//失败后处理filter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);filter.setAuthenticationManager(authenticationManagerBean());return filter;}}

到此为止后端的一些配置全部结束,一些需要用到的 额外的类 根据自己的 实际业务来引入即可。

前端方面只需配置下跨域请求,和每次请求携带上 token 令牌即可。

用户登录成功之后,将响应来的 token 存入 sessionStorage中,以后每次请求前端都会携带该token给服务器端。

         let userInfo = result.data.data;// 移除当前session里面的用户window.sessionStorage.removeItem("userInfo");// 将新用户的信息存入sessionwindow.sessionStorage.setItem("userInfo", JSON.stringify(userInfo));console.log(JSON.parse(sessionStorage.getItem("userInfo")))// 登录成功将 token 中的数据 存入 每一次 axios的请求头内部 !!!!!!!! 超级重点//将token放到axios的headers中axios.defaults.headers.common["Authorization"] = userInfo.token;

总结: 前后端分离的项目下springSecurity 大部分过滤器都需要自己自定义来实现,实现动态的权限控制,前端只需每次请求携带 token 即可!!!

前后端分离开发下的权限管控 :SpringSecurity 框架相关推荐

  1. 前后端分离模式下的权限设计方案

    前后端分离模式下,所有的交互场景都变成了数据,传统业务系统中的权限控制方案在前端已经不再适用,因此引发了我对权限的重新思考与设计. 权限控制到底控制的是什么? 在理解权限控制之前,需要明白两个概念:资 ...

  2. Nodejs搭建前后端分离开发模式下的微信网页项目

    原文链接:<Nodejs搭建前后端分离开发模式下的微信网页项目>- 陈帅华 本文涉及对前后端分离及微信网页项目中的前端如何在本地环境中开发与调试的思考. 主要问题 1.如何配置微信公众平台 ...

  3. 视频教程-SpringBoot+Security+Vue前后端分离开发权限管理系统-Java

    SpringBoot+Security+Vue前后端分离开发权限管理系统 10多年互联网一线实战经验,现就职于大型知名互联网企业,架构师, 有丰富实战经验和企业面试经验:曾就职于某上市培训机构数年,独 ...

  4. .NET Core开发实战(第23课:静态文件中间件:前后端分离开发合并部署骚操作)--学习笔记(下)...

    23 | 静态文件中间件:前后端分离开发合并部署骚操作 这里还有一个比较特殊的用法 一般情况下,我们前后端分离的架构,前端会编译成一个 index.html 文件和若干个 CSS 文件和 JavaSc ...

  5. 前后端分离开发模式下后端质量的保证 —— 单元测试

    概述 在今天, 前后端分离已经是首选的一个开发模式.这对于后端团队来说其实是一个好消息,减轻任务并且更专注.在测试方面,就更加依赖于单元测试对于API以及后端业务逻辑的较验.当然单元测试并非在前后端分 ...

  6. ultraedit 运行的是试用模式_单元测试 —— 前后端分离开发模式下后端质量的保证...

    概述 在今天, 前后端分离已经是首选的一个开发模式.这对于后端团队来说其实是一个好消息,减轻任务并且更专注.在测试方面,就更加依赖于单元测试对于API以及后端业务逻辑的较验.当然单元测试并非在前后端分 ...

  7. Springboot + Spring Security 实现前后端分离登录认证及权限控制

    Spring Security简介 Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展 ...

  8. SpringBoot + Vue前后端分离开发:全局异常处理及统一结果封装

    SpringBoot + Vue前后端分离开发:全局异常处理及统一结果封装 文章目录 SpringBoot + Vue前后端分离开发:全局异常处理及统一结果封装 前后端分离开发中的异常处理 统一结果封 ...

  9. 视频教程-SpringBoot2+Vue+AntV前后端分离开发项目实战-Java

    SpringBoot2+Vue+AntV前后端分离开发项目实战 10多年互联网一线实战经验,现就职于大型知名互联网企业,架构师, 有丰富实战经验和企业面试经验:曾就职于某上市培训机构数年,独特的培训思 ...

最新文章

  1. cannot import name #39get_all_providers#39
  2. cfnet用于嵌入式_做嵌入式驱动的,你一定要挺住!
  3. python判断对象是否实例化_python中如何判断class当前有哪些实例?
  4. Echarts地图添加自定义图标
  5. C#自定义数字格式字符串
  6. MathType requires a newer version of MT Extra等MathType问题的不兼容性解决方案
  7. 浏览器显示html过程,浏览器显示页面的流程
  8. linux中配置Java环境
  9. 金山pdf阅读器 独立版V10.1.0.6683
  10. IDEA2019安装及PJ
  11. autoCAD2014安装激活
  12. 这样拆分和压缩css代码
  13. 2021智能零售领域最具商业合作价值企业盘点
  14. 和计算机相关的英文名字女孩,最好听的英文名字女孩
  15. 2022面试Android之单例模式
  16. 计算机辅助项目管理实验论文,计算机辅助项目管理B卷
  17. html中照片摆心形,九张照片墙怎么摆心形
  18. 门后的秘密-读书笔记
  19. Android NFC技术(三)——初次开发Android NFC你须知道NdefMessage和NdefRecord
  20. getitemany

热门文章

  1. 公安局计算机岗位应知应会综合基础知识,事业单位考试计算机综合知识基础知识真题...
  2. Hack The Box——Remote
  3. java总是permgen out_java.lang.OutOfMemoryError: PermGen space及其解决方法
  4. 安卓SDK——人脸识别
  5. JavaWeb25.3【综合案例:注册功能(含邮箱激活账号)】
  6. CSS设置图像的透明度
  7. GPRS网络几种数据中心的接入方式
  8. Android 第三次作业 contentprovider与resolver
  9. GeneMark-ES:真核生物编码基因预测软件
  10. Application应用程序