认证原理分析
:::tips
Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。 当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此 类,下图是Spring Security过虑器链结构图
:::
:::tips
FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时 这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认 证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager)进行处理。
:::
使用步骤:
:::tips
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
:::
自定义Security认证过滤器
import com.fasterxml.jackson.databind.ObjectMapper;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.AbstractAuthenticationProcessingFilter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.awt.*;import java.io.IOException;import java.util.HashMap;/*** @author by itheima* @Date 2022/1/21* @Description*/public class MyUserNamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {/*** 设置构造,传入自定义登录url地址* @param defaultFilterProcessesUrl*/public MyUserNamePasswordAuthenticationFilter(String defaultFilterProcessesUrl) {super(defaultFilterProcessesUrl);}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {//从post请求流中获取登录信息String username=null;String password=null;try {//判断 是否是ajax登录if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)) {ServletInputStream in = request.getInputStream();HashMap<String,String> map = new ObjectMapper().readValue(in, HashMap.class);username=map.get("username");password=map.get("password");}} catch (IOException e) {e.printStackTrace();}//生成认证tokenUsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);//交给认证管理器取认证tokenreturn this.getAuthenticationManager().authenticate(token);}/*** 认证成功处理方法* @param request* @param response* @param chain* @param authResult* @throws IOException* @throws ServletException*/@Overrideprotected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult) throws IOException, ServletException {//认证主体信息UserDetails principal = (UserDetails) authResult.getPrincipal();//组装响应前端的信息String username = principal.getUsername();String password = principal.getPassword();Collection<? extends GrantedAuthority> authorities = principal.getAuthorities();//构建JwtTokenString token = JwtTokenUtil.createToken(username, new Gson().toJson(authorities));HashMap<String, String> info = new HashMap<>();info.put("name",username);info.put("token",token);//设置响应格式response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.getWriter().write(new Gson().toJson(info));}/*** 认证失败处理方法* @param request* @param response* @param failed* @throws IOException* @throws ServletException*/@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response,AuthenticationException failed) throws IOException, ServletException {String returnData="";// 账号过期if (failed instanceof AccountExpiredException) {returnData="账号过期";}// 密码错误else if (failed instanceof BadCredentialsException) {returnData="密码错误";}// 密码过期else if (failed instanceof CredentialsExpiredException) {returnData="密码过期";}// 账号不可用else if (failed instanceof DisabledException) {returnData="账号不可用";}//账号锁定else if (failed instanceof LockedException) {returnData="账号锁定";}// 用户不存在else if (failed instanceof InternalAuthenticationServiceException) {returnData="用户不存在";}// 其他错误else{returnData="未知异常";}// 处理编码方式 防止中文乱码response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);// 将反馈塞到HttpServletResponse中返回给前台R result = R.error(returnData);response.getWriter().write(new Gson().toJson(result));}}
定义获取用户详情服务bean
import com.itheima.entity.TbUser;import com.itheima.mapper.TbUserMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.AuthorityUtils;import org.springframework.security.core.userdetails.User;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.Service;import java.util.List;/*** @author by itheima* @Date 2022/5/23* @Description*/@Service("userDetailsService")public class MyUserDetailServiceImpl implements UserDetailsService {@Autowiredprivate TbUserMapper tbUserMapper;/*** 使用security当用户认证时,会自动将用户的名称注入到该方法中* 然后我们可以自己写逻辑取加载用户的信息,然后组装成UserDetails认证对象* @param userName* @return 用户的基础信息,包含密码和权限集合,security底层会自动比对前端输入的明文密码* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {//1.根据用户名称获取用户的账户信息TbUser dbUser=tbUserMapper.findUserInfoByName(userName);//判断该用户是否存在if (dbUser==null) {throw new UsernameNotFoundException("用户名输入错误!");}//2.组装UserDetails对象//获取当前用户对应的权限集合(自动将以逗号间隔的权限字符串封装到权限集合中)List<GrantedAuthority> list = AuthorityUtils.commaSeparatedStringToAuthorityList(dbUser.getRoles());/*参数1:账户参数2:密码参数3:权限集合*/User user = new User(dbUser.getUsername(), dbUser.getPassword(), list);return user;}}
定义SecurityConfig类
/*** 配置授权策略* @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http//坑-过滤器要添加在默认过滤器之前,否则,登录失效.addFilterBefore(myUserNamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).csrf().disable();}@Beanpublic MyUserNamePasswordAuthenticationFilter myUserNamePasswordAuthenticationFilter() throws Exception {//设置默认登录路径MyUserNamePasswordAuthenticationFilter myUserNamePasswordAuthenticationFilter =new MyUserNamePasswordAuthenticationFilter("/authentication/form");myUserNamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());return myUserNamePasswordAuthenticationFilter;}
自定义权限认证过滤
import com.google.gson.Gson;import com.itheima.security.utils.JwtTokenUtil;import org.springframework.http.MediaType;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.authority.AuthorityUtils;import org.springframework.security.core.context.SecurityContextHolder;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;import java.util.HashMap;/*** @author by itheima* @Date 2022/1/23* @Description 权限认证filter*/public class AuthenticationFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {//1.从http请求头中获取tokenString token = request.getHeader(JwtTokenUtil.TOKEN_HEADER);if (token==null) {//用户未登录,则放行,去登录拦截filterChain.doFilter(request,response);return;}//2.token存在则,安全校验try {String username = JwtTokenUtil.getUsername(token);//获取以逗号间隔的权限拼接字符串String userRole = JwtTokenUtil.getUserRole(token);//组装tokenUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, AuthorityUtils.commaSeparatedStringToAuthorityList(userRole));//将生成的token存入上下文SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行资源filterChain.doFilter(request,response);} catch (Exception e) {e.printStackTrace();// throw new RuntimeException("token无效");response.setContentType(MediaType.APPLICATION_JSON_VALUE);HashMap<String, String> info = new HashMap<>();info.put("status","0");info.put("ex","无效的token凭证");response.getWriter().write(new Gson().toJson(info));}}}
配置授权拒绝策略
自定义访问拒绝处理器
import com.google.gson.Gson;import com.itheima.stock.vo.resp.R;import com.itheima.stock.vo.resp.ResponseCode;import org.springframework.http.MediaType;import org.springframework.security.access.AccessDeniedException;import org.springframework.security.web.access.AccessDeniedHandler;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/*** 认证用户无权限访问资源时处理器*/public class StockAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {//设置响应数据格式response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);//构建结果R result = R.error(ResponseCode.NOT_PERMISSION.getCode(),ResponseCode.NOT_PERMISSION.getMessage());//将对象序列化为json字符串响应前台response.getWriter().write(new Gson().toJson(result));}}
自定义匿名用户拒绝处理器
import com.google.gson.Gson;
import com.itheima.stock.vo.resp.R;
import com.itheima.stock.vo.resp.ResponseCode;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 匿名用户(即未登录时访问资源为匿名访问)无权限处理器
*/
public class AccessAnonymousEntryPoint implements AuthenticationEntryPoint {
/**
* 当用户请求了一个受保护的资源,但是用户没有通过认证,那么抛出异常,
* AuthenticationEntryPoint. Commence(..)就会被调用。
* @param request
* @param response
* @param authException
* @throws IOException
* @throws ServletException
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//设置响应数据格式
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
//构建结果
R result = R.error(ResponseCode.NOT_PERMISSION.getCode(),ResponseCode.NOT_PERMISSION.getMessage());
//将对象序列化为json字符串响应前台
response.getWriter().write(new Gson().toJson(result));
}
}
整体流程小结


