认证原理分析
:::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);
}
@Override
public 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();
}
//生成认证token
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
//交给认证管理器取认证token
return this.getAuthenticationManager().authenticate(token);
}
/**
* 认证成功处理方法
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected 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();
//构建JwtToken
String 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
*/
@Override
protected 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 {
@Autowired
private TbUserMapper tbUserMapper;
/**
* 使用security当用户认证时,会自动将用户的名称注入到该方法中
* 然后我们可以自己写逻辑取加载用户的信息,然后组装成UserDetails认证对象
* @param userName
* @return 用户的基础信息,包含密码和权限集合,security底层会自动比对前端输入的明文密码
* @throws UsernameNotFoundException
*/
@Override
public 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
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//坑-过滤器要添加在默认过滤器之前,否则,登录失效
.addFilterBefore(myUserNamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable();
}
@Bean
public 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 {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//1.从http请求头中获取token
String 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);
//组装token
UsernamePasswordAuthenticationToken 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 {
@Override
public 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));
}
}