目录
1、引入依赖
2、创建类继承WebSecurityConfigurerAdapter
(1)重写里面的configure(HttpSecurity http)方法
(2)重写AuthenticationManager authenticationManagerBean()
(3)密码加密工具
3、继承UserDetails
4、登录方法
5、是怎么完成登录的
(1)根据用户名查询用户
(2)密码对比
6、注册用户加密密码
7、登录过滤器
8、认证失败处理器
ps:该文章适合未系统学习springsecurity快速使用,可以直接cv使用,只有部分源码讲解,个人觉得先会用了再深究原理
1、引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>2.6.13</version></dependency>
引入springsecurity依赖后,该依赖会自动生成默认登陆页面和登录名(user)和密码(控制台)
我们使用这个用户名和密码登陆后才可以对资源进行访问
因为项目是前后端分离项目,因此并不需要它默认生成的登录页面和默认用户名密码吗,需要查询数据库进行登录
2、创建类继承WebSecurityConfigurerAdapter
我们创建一个配置类SecurityConfig(使用@Configuration标记)并且继承WebSecurityConfigurerAdapter类,重写里面的几个配置方法即可对springsecurity进行配置
(1)重写里面的configure(HttpSecurity http)方法
跟进configure(HttpSecurity http)查看源码中的方法做了什么
我们可以看到源码中的该方法中默认对所有的请求进行拦截,并且默认生成表单登录页面,并且使用基本认证。
我们只需要重写该方法就可以自己进行配置了
.csrf().disable() .cors() csrf建议关闭,cors前后端分离项目建议打开
.mvcMatchers("/admin/login").anonymous() 对这个接口可以匿名访问,也就是不需要认证
.mvcMatchers("/admin/save").permitAll() 对这个接口也不做认证
.mvcMatchers("/user/save").authenticated() 对这个接口需要认证才能访问这个.mvcMatchers的参数也可以是数组形式
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)添加过滤器,这里的tokenAuthenticationFilter是我自己定义的,这个意思将自定义的这个过滤器放在UsernamePasswordAuthenticationFilter.class这个过滤器之前,这个过滤器是springsecurity提供的认证过滤器,像我们的这个tokenAuthenticationFilter是需要在认证之前进行的
.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint);这个是添加了一个认证异常处理器authenticationEntryPoint,authenticationEntryPoint也是我们自定义的,就是当用户未经过认证时返回的结果,通常当未登录访问接口时返回给前端的异常信息就在这里定义
这样我们就大致完成了这个方法中登录认证功能的一些配置
(2)重写AuthenticationManager authenticationManagerBean()
我们需要使用他里面的方法进行登录认证,并使用@Bean标注到spring容器中
@Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean(); }
(3)密码加密工具
@Bean public PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder(); }
使用这个进行密码加密,springsecurity提供了很多密码加密方法,用这个就可以,可以点进去查看PasswordEncoder这个方法,这是个接口,实现了很多加密方法
然后这样我们就大致完成了这个类的配置
如果有swagger等静态资源配置,可以重写这个方法
/*** 配置哪些请求不拦截* 排除swagger相关请求* @param web* @throws Exception*/ @Override public void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html"); }
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate TokenAuthenticationFilter tokenAuthenticationFilter;@Autowiredprivate AuthenticationEntryPointImpl authenticationEntryPoint;/* @Autowiredprivate UserDetailsService userDetailsService;*/@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().cors().and().authorizeRequests().mvcMatchers("/admin/login").anonymous().mvcMatchers("/admin/save").permitAll().mvcMatchers("/wx/user/login").permitAll().mvcMatchers("/wx/user/save").permitAll().anyRequest().authenticated().and().addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class).exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);}@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}/*** 配置哪些请求不拦截* 排除swagger相关请求* @param web* @throws Exception*/@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");}}
防止报错还有这个自定义的登录拦截器跟认证失败处理器也整上
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate StringRedisTemplate redisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("token");if (ObjectUtils.isEmpty(token)){filterChain.doFilter(request,response);return;}Claims claims = null;try {claims = JwtUtil.parseJWT(token);} catch (Exception e) {e.printStackTrace();Map<String, String> errMsg = new HashMap<>();errMsg.put("code","200");errMsg.put("msg","访问失败,请重新登录");response.setContentType("text/json;charset=utf-8");response.getWriter().print(errMsg.toString());return;}Integer userId = Integer.valueOf(claims.getSubject());UserContext.setUser(userId);String userAdmin = redisTemplate.opsForValue().get("userId" + userId);AdminLogin adminLogin = JSONUtil.toBean(userAdmin, AdminLogin.class);UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(adminLogin.getUsername(), adminLogin.getUsername(), null);
// UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(null, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request,response);}
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{private static final long serialVersionUID = -8970718410437077606L;@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)throws IOException{Map<String, String> errMsg = new HashMap<>();response.setContentType("text/json;charset=utf-8");errMsg.put("code","200");errMsg.put("msg","访问失败,该资源受到保护...");response.getWriter().print(errMsg.toString());}
}
等会再说这两个配置
3、继承UserDetails
用我们的登录的用户类继承UserDetails,我这里是Admin
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AdminLogin implements UserDetails {private Admin admin;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return admin.getPassword();}@Overridepublic String getUsername() {return admin.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
我们得重写里面的几个方法,并且把我们的用户类给整进来
Collection<? extends GrantedAuthority> getAuthorities()这个是权限,我们返回null就行了,这会登录用不到
public String getPassword() {return admin.getPassword();}这个方法是获取密码,也就是security会从这里获取登录的密码,我们就把我们的用户类的密码让他返回
public String getUsername() { return admin.getUsername();}这个是获取用户名的方法,也就是security会从这里获取登录的用户名,我们就把我们的用户类的用户名让他返回
@Override public boolean isAccountNonExpired() {return true; }@Override public boolean isAccountNonLocked() {return true; }@Override public boolean isCredentialsNonExpired() {return true; }@Override public boolean isEnabled() {return true; }这几个都是账号相关的,什么账号是否被锁定、是否启用在这里返回结果,我们返回true,如果返回false就登录不了了
4、登录方法
@Service
public class AdminLoginServiceImpl implements AdminLoginService {@Autowiredprivate AuthenticationManager authenticationManager;//管理员登录@Overridepublic Result adminLogin(LoginDto loginDto) {UsernamePasswordAuthenticationToken authenticationToken = newUsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);AdminLogin adminLogin = (AdminLogin) authenticate.getPrincipal();String jwt = JwtUtil.createJWT(String.valueOf(adminLogin.getAdmin().getId()));//用户信息redisTemplate.opsForValue().set("userId"+adminLogin.getAdmin().getId(), JSONUtil.toJsonStr(adminLogin));return Result.success(jwt);}
}
登录的方法就是调用
Authentication authenticate = authenticationManager.authenticate(authenticationToken);它里面需要接受的参数类型必须是Authentication类型的
这就是为啥在配置的时候将
AuthenticationManager authenticationManagerBean()这个使用@Bean标记
UsernamePasswordAuthenticationToken authenticationToken = new
UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword());这个类就可以将我们的用户名和密码封装成继承了Authentication类型的类然后用于登录
UsernamePasswordAuthenticationToken()他的参数是
Object principal, Object credentials这就分别是用户名和登陆凭证也就是密码
然后登陆成功后返回一个Authentication类型,然后.getPrincipal()这个方法就可以获取登录的用户信息。
5、是怎么完成登录的
这时候我们来看登录流程图(图是盗的)
当我们调用Authentication authenticate = authenticationManager.authenticate(authenticationToken);这个方法的时候做了什么?
我们关注两步就可以了
第一个就是根据用户名查询用户,第二个就是进行密码比对
(1)根据用户名查询用户
因为在执行认证的方法后,会调用DaoAuthencationProvider中的UserDetailService对象中的loadUserByUsername这个方法,如果基于springsecurity的默认配置,这个方法就是实现了UserDetailService这个接口的InMemoryUserDetailsManager这个方法中的loadUserByUsername
我们可以看到进行登录时候调用了loadUserByUsername的方法,这个方法是在
UserDetailsService中写的,看方法名也知道是根据用户名查找用户
因为我们要查的是数据库中的用户数据,我们就可以也可以实现UserDetailService并且重写里面的loadUserByUsername方法 根据数据库查询出用户信息并返回继承了UserDetail的AdminLogin 类
@Service
public class AdminDetailsServiceImpl implements UserDetailsService {@Autowiredprivate AdminService adminService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查询数据库中的用户LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();wrapper.eq(Admin::getUsername,username);Admin admin = adminService.getOne(wrapper);//如果根据用户名查找不到用户if (ObjectUtils.isEmpty(admin)){throw new TxdException(208,"用户不存在");}//返回adminLoginAdminLogin adminLogin = new AdminLogin(admin);return adminLogin;}
}
如果能查到用户就返回,然后进行下一步密码对比,如果用户不存在就抛异常
(2)密码对比
这个springsecurity都已经写好了,我们看看源码就行,找到那个我们自定义的security的配置类
ctrl点进BCryptPasswordEncoder加密方式
里面的boolean matches(CharSequence rawPassword, String encodedPassword)这个方法就是密码对比,它里面又会执行BCrypt.checkpw(rawPassword.toString(), encodedPassword)这个方法。总之就是将前端传来的密码进行加密后与数据库的进行对比为啥不能将数据库的密码解析后对比传来的明文密码呢?因为他这个加密之后是不可逆的
然后到这登录基本就完事了
新问题,数据库中还没加密后的用户数据怎么办?
6、注册用户加密密码
将前端传来的密码使用security配置类中的加密方式加密后就行
7、登录过滤器
在前面配置的时候已经整过代码了,在这里获取token并校验,校验完之后获取里面的用户id,根据用户id获取redis里面的数据,并将用户信息使用UsernamePasswordAuthenticationToken 封装并且放入SecurityContextHolder.getContext().setAuthentication(authenticationToken);中
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate StringRedisTemplate redisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("token");if (ObjectUtils.isEmpty(token)){filterChain.doFilter(request,response);return;}Claims claims = null;try {claims = JwtUtil.parseJWT(token);} catch (Exception e) {e.printStackTrace();Map<String, String> errMsg = new HashMap<>();errMsg.put("code","200");errMsg.put("msg","访问失败,请重新登录");response.setContentType("text/json;charset=utf-8");response.getWriter().print(errMsg.toString());return;}Integer userId = Integer.valueOf(claims.getSubject());UserContext.setUser(userId);String userAdmin = redisTemplate.opsForValue().get("userId" + userId);AdminLogin adminLogin = JSONUtil.toBean(userAdmin, AdminLogin.class);UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(adminLogin.getUsername(), adminLogin.getUsername(), null);
// UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(null, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request,response);}
}
8、认证失败处理器
当用户未认证时访问资源提示的信息
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{private static final long serialVersionUID = -8970718410437077606L;@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)throws IOException{Map<String, String> errMsg = new HashMap<>();response.setContentType("text/json;charset=utf-8");errMsg.put("code","200");errMsg.put("msg","访问失败,该资源受到保护...");response.getWriter().print(errMsg.toString());}
}