一,流程图
-- 来自尚硅谷
二,类的说明情况(便于理解使用Spring Security)
UsernamePasswordAuthenticationFilter 的attemptAuthentication方法 : 获取用户名和密码
UsernamePasswordAuthenticationToken类 : 封装 用户名和密码
ProviderManager类authenticate方法 进行认证 调用authenticate方法
认证核心类 AbstractUserDetailsAuthrnticationProvider的authenticate方法 调用retrieveUser方法
AbstractUserDetailsAuthrnticationProvider的retrieveUser方法 中调用 loadUserByUsername()方法查询数据路 返回UserDetails对象
认证核心类 AbstractUserDetailsAuthrnticationProvider的authenticate方法 调用additionalAuthenticationChecks()方法 进行密码校验
认证核心类 AbstractUserDetailsAuthrnticationProvider的createSuccessAuthntication方法数据回填到 返回result
UsernamePasswordAuthenticationFilter的父类 AbstractAuthenticationProcessingFilter的doFilter方法调用 successfulAuthentication方法 把这个返回值result对象放进上下文对象中 供项目使用
三,使用Spring Security
组件的自定义
- userDeatils --封装对象 根据你的需求更改
- loadUserByUsername --根据用户名查询对象
- additionalAuthenticationChecks里的密码校验器passwordEncoder方法 --重写校验密码 看你需不需要
具体核心组件
- 登录Filter: 判断用户名和密码是否正确 生成token
- 认证解析token组件:判断请求头里面是否有token 如果有认证完成
- 在配置类中配置相关类
四,代码
passwordEncoder类 使用MD5工具类加密处理
package com.atguigu.security.custom;
import com.atguigu.com.md5.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 密码处理
*/
@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
@Override
// 将密码 进行MD5加密
public String encode(CharSequence charSequence) {
return MD5.encrypt(charSequence.toString());
}
@Override
// 判断 密码是否相同
public boolean matches(CharSequence charSequence, String s) {
return s.equals(MD5.encrypt(charSequence.toString()));
}
}
userDeatis 类
为什么代码里卖弄继承了User 因为User继承了UserDetails
package com.atguigu.security.custom;
import com.atguigu.model.system.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class CustomUser extends User {
// 自己对象类
private SysUser sysUser;
public CustomUser(SysUser sysUser , Collection<? extends GrantedAuthority> authorities) {
super(sysUser.getUsername(), sysUser.getPassword(), authorities);
}
public SysUser getSysUser(){
return sysUser;
}
public void setSysUser(SysUser sysUser){
this.sysUser = sysUser;
}
}
业务对象UserDetailsService 查询数据库
package com.atguigu.security.custom;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
@Component
public interface UserDetailsService extends org.springframework.security.core.userdetails.UserDetailsService {
/**
* 跟还有用户名获取用户对象 (获取不直接报异常)
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
package com.atguigu.auth.service.impl;
import com.atguigu.auth.service.SysUserService;
import com.atguigu.model.system.SysUser;
import com.atguigu.security.custom.CustomUser;
import com.atguigu.security.custom.UserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUsername(username);
if(sysUser == null){
throw new UsernameNotFoundException("用户名不存在");
}
if(sysUser.getStatus().intValue() == 0){
throw new RuntimeException("账号已停用");
}
return new CustomUser(sysUser ,
Collections.emptyList());
}
}
filter 用户登录过滤器
package com.atguigu.security.filter;
import com.atguigu.com.jwt.JwtHelper;
import com.atguigu.com.result.ResponseUtil;
import com.atguigu.com.result.Result;
import com.atguigu.com.result.ResultCodeEnum;
import com.atguigu.model.process.VO.LoginVO;
import com.atguigu.security.custom.CustomUser;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.ws.Response;
import java.io.IOException;
import java.util.HashMap;
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
// 构造方法
// TODO: 个构造方法接收一个参数 AuthenticationManager,
// 它是Spring Security中的一个关键接口,负责处理验证用户身份的过程。
// 这个构造方法的目的是初始化一个TokenLoginFilter实例,并传入认证管理器以便在过滤器中使用。
public TokenLoginFilter(AuthenticationManager authenticationManager){
//TODO: 这行代码将传入的 AuthenticationManager 实例设置到当前过滤器中。
// 这是至关重要的一步,因为后续的认证逻辑需要依赖这个认证管理器来验证用户的登录凭证。
this.setAuthenticationManager(authenticationManager);
// 此调用设置了过滤器只处理HTTP POST请求的标志为false。默认情况下,某些过滤器可能只允许POST请求(例如表单登录),但这里显式地将其关闭,
// 意味着此过滤器不限制HTTP方法,可能是因为它不仅仅用于处理标准的表单登录。
this.setPostOnly(false);
//指定登录接口及提交方式 可以指定任意路径 这行代码配置了过滤器应该关注的特定请求路径及其HTTP方法。
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
}
// 登录认证
// 获取输入的用户名和密码 调用方法认证
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
// todo:获取用户输入的信息
// request.GetIntputStream(): 从http请求中获取输入流 json数据
// ObjectMapper 创建Jackson库实例 将json数据 Java对象进行序列化和反序列化的操作
// readValue(A , B) 将json对象反序列化为java对象
LoginVO loginVO = new ObjectMapper().readValue(request.getInputStream(), LoginVO.class);
//todo: 封装对象
UsernamePasswordAuthenticationToken AuthenticationToken = new UsernamePasswordAuthenticationToken(loginVO.getUsername(), loginVO.getPassword());
//todo: 调用方阿飞
return this.getAuthenticationManager().authenticate(AuthenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//认证成功调用的方法
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// 获取当前用户
CustomUser principal = (CustomUser) authResult.getPrincipal();
// 生成token
String username = principal.getSysUser().getUsername();
String token = JwtHelper.createToke(principal.getSysUser().getId(),
username);
// 返回
HashMap<String, Object> map = new HashMap<>();
map.put("token",token);
ResponseUtil.out(response, Result.ok(map));
}
//认证失败调用的方法
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response ,Result.build(null , ResultCodeEnum.LOGIN_ERROR));
}
}
验证过滤器
package com.atguigu.security.filter;
import com.atguigu.com.jwt.JwtHelper;
import com.atguigu.com.result.ResponseUtil;
import com.atguigu.com.result.Result;
import com.atguigu.com.result.ResultCodeEnum;
import org.apache.catalina.util.RequestUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.jaas.JaasAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.ServletRequestUtils;
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.Collections;
public class TokenAuthenticationFilter extends OncePerRequestFilter {
// OncePerRequestFilter 这个类是一个自定义的过滤器实现,旨在确保对每个HTTP请求只进行一次特定的过滤处理,
// 通过设置请求属性标志来避免重复执行,并提供灵活的跳过逻辑和错误处理机制,是Java Web应用中处理请求预处理和后处理的典型模式。
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
logger.info("请求路径uri" + httpServletRequest.getRequestURI());
// 判断是否为登录接口
if("/admin/system/index/login".equals(httpServletRequest.getRequestURI())){
// 相同 登录 放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
return ;
}
// 如果不是登录 就要验证token值
UsernamePasswordAuthenticationToken authentication = getAuthentication(httpServletRequest);
if(authentication != null){
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(httpServletRequest , httpServletResponse);
} else {
ResponseUtil.out(httpServletResponse, Result.build(null , ResultCodeEnum.LOGIN_ERROR));
}
}
// 获取token 验证token
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){
// 从header中获取token
String token = request.getHeader("token");
logger.info("token:" + token);
if(!StringUtils.isEmpty(token)){
String username = JwtHelper.getUsername(token);
logger.info("用户名:" + username);
if(!StringUtils.isEmpty(username)){
// 参数一:principal 主要标识 用户名
// 参数二:credentials 代表用户的凭证,最常见的情况是明文密码。但在实际安全实践中,密码应当经过加密处理,此参数也可能是一个密码的散列值或其他形式的凭证。
// 参数三: 集合 authorities: 类型为Collection<? extends GrantedAuthority>,表示用户具有的权限集合。
// GrantedAuthority接口代表用户的一个权限,比如"ROLE_ADMIN"、"ROLE_USER"等。这个集合定义了用户登录后能访问哪些资源或执行哪些操作
return new UsernamePasswordAuthenticationToken(username ,null , Collections.emptyList());
}
}
return null;
}
}
创建配置类
package com.atguigu.security.config;
import com.atguigu.security.custom.CustomMd5PasswordEncoder;
import com.atguigu.security.custom.UserDetailsService;
import com.atguigu.security.filter.TokenAuthenticationFilter;
import com.atguigu.security.filter.TokenLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
@Configuration
@EnableWebSecurity //@EnableWebSecurity 是开启SpringSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 查询数据库 接口
@Autowired
private UserDetailsService userDetailsService;
// 密码判断 实现类
@Autowired
private CustomMd5PasswordEncoder customMd5PasswordEncoder;
@Bean
protected AuthenticationManager authenticationManager() throws Exception{
return super.authenticationManager();
}
protected void configure(HttpSecurity http) throws Exception {
//这是配置的关键 决定哪些接口开启防护 哪些接口绕过防护
http
// 关闭csrf跨站请求伪造
.csrf().disable()
// 开启跨域 让前端接口进行调试
.cors().and()
.authorizeRequests()
//指定某些接口不需要通过验证即可访问 登录接口肯定是不需要认证的
.antMatchers("/admin/system/index/login").permitAll()
//这里的意思是让其他所有接口需要认证才能访问
.anyRequest().authenticated()
.and()
//TokenAuthenticationFilter放到usernamePasswordAuthentcation的Filter的前面 目的: 这样做除了登录认证查询数据库 其他时候token认证
.addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilter(new TokenLoginFilter(authenticationManager()));
// 禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
// 指定userDetailService加密器
auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);
}
/**
* 配置哪些请求不拦截
* 排除swagger相关请求
* @Param web
*
*/
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/favicon,ico","/swagger-resources/**","/webjars/**","/v2/**","/swagger-ui.html","/doc.htl");
}
}
五,流程
TokemAuthenticationFilter
- 判断路径 是否为登录
- 是:
TokenLoginFilter
- 获取我们用户信息
- 封装
- authenticate方法进行认证
UserDeatilsService
- 根据用户名查询用户信息 校验密码
TokenLoginFilter
- succcessfulAuthentication 生成token
TokemAuthenticationFilter
- 判断路径 是否为登录
- 不是:
- 查看header中是否有token