目录
一 作用
二 流程及源码分析
一 作用
spring security作为spring家族中的一员,它的主要作用有两个,分别是认证和授权。
我们以前在实现登录功能的时候,前端会传来用户名和密码,然后我们根据前端传来的数据从用户表中的数据进行比较,从而实现用户登录。
而springSecurity的功能也有登录认证,并且它在登录认证成功后,会生成一个认证对象,当登录成功后再发送其他请求,就会根据这个认证对象来判断当前用户是否已经登录。
二 流程及源码分析
在使用springSecurity之前,你还需要导入它的依赖坐标,只要导入了依赖,它就会自动生效。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
① 用户输入用户名和密码以及验证码访问登录接口:
登录其实也是一个请求,所以也会被springsecurity拦截,但是因为你没有登录,就没有他的认证对象,它就不会让你访问这个接口,所以开始之前还需要在spring security的配置类中放行登录请求。(配置类的代码最后会统一给)
放行登录亲请求:
② 调用逻辑业务service层,完成验证。
package com.fs.system.service.ipml;import cn.hutool.core.convert.Convert; import com.fs.common.constant.CacheConstants; import com.fs.common.constant.UserConstants; import com.fs.common.core.pojo.SysUser; import com.fs.common.core.vo.LoginUser; import com.fs.common.enums.UserStatus; import com.fs.common.exception.ServiceException; import com.fs.common.exception.user.BlackListException; import com.fs.common.exception.user.CaptchaException; import com.fs.common.exception.user.UserNotExistsException; import com.fs.common.exception.user.UserPasswordNotMatchException; import com.fs.common.util.DateUtils; import com.fs.common.util.RedisCache; import com.fs.common.util.ip.IpUtils; import com.fs.system.security.UserDetailsServiceImpl; import com.fs.system.service.ISysConfigService; import com.fs.system.service.ISysLoginService; import com.fs.system.service.ISysUserService; import com.mysql.cj.Constants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service;import java.util.Objects;@Service public class ISysLoginServiceImpl implements ISysLoginService {@Autowiredprivate ISysUserService sysUserService ; //进行用户信息校验,包括查询该用户是否存在@Autowiredprivate SysPasswordService passwordService ; //密码校验,包括密码输入的正确性已经输入密码的次数@Autowiredprivate ISysConfigService sysConfigService ; //判断验证码功能有没有开启@Autowiredprivate RedisCache redisCache ; //用于操作缓存中的数据@Autowiredprivate TokenService tokenService ; //对token的操作@Autowiredprivate UserDetailsServiceImpl userDetailsService ; //用来获取LoginUser对象@Autowiredprivate AuthenticationManager authenticationManager ; //用于获取认证/** 登录验证 */@Overridepublic String login(String username, String password, String code, String uuid) { // 1.验证码验证码validateCaptcha(username , code , uuid);System.out.println("验证码校验完成..."); // 2.参数校验loginCheck(username , password) ;3.根据用户名查询用户 // SysUser sysUser = sysUserService.selectUserByUserName(username);// 检验密码 // passwordService.validate(sysUser , password); //通过Security帮我们进行处理,不需要自己校验了// 创建token // //先创建一个LoginUser对象,因为返回的的是LoginUser对象 // LoginUser loginUser = new LoginUser(); // loginUser.setUserId(sysUser.getUserId()); // loginUser.setUser(sysUser);// 返回一个认证对象Authentication authentication = null;try{//创建一个Authentication认证对象, 属性: authenticated =false, 没有被认证UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);//返回一个认证对象: 被认证认证对象authentication = authenticationManager.authenticate(authRequest); //这个认证对象就是一个登录的认证器}catch (Exception e){throw new ServiceException("用户不存在或者是密码错误");}//调用tokenService创建token//里面已经给LoginUser赋值token了,不过这里面的token是一个uuid,redis存的是loginUser,LoginUser里面的token又是一个uuid?String token = tokenService.createToken((LoginUser) userDetailsService.loadUserByUsername(username)); // 修改登陆时间和登录ipLoginUser loginUser= (LoginUser) authentication.getPrincipal();recordLoginInfo(loginUser.getUserId()); // 返回token给前端(token里面存放着登录用户的信息,)System.out.println("token:=========>"+token);return token;}/** 验证码验证码 */@Overridepublic void validateCaptcha(String username, String code, String uuid) {/*** code是前端传来的验证码答案,uuid是验证码缓存的key*/ // 先判断有没有开启验证码功能boolean captchaEnabled = sysConfigService.selectCaptchaEnabled();if (!captchaEnabled) throw new ServiceException("验证码功能没有开启"); // 然后根据key去查询缓存中的value,判断value和输入的答案是否正确String value = redisCache.getCacheObject(CacheConstants.CAPTCHA_CODE_KEY + uuid); // 再将从缓存中拿到的value和code比对if (!value.equals(code)){throw new CaptchaException();}}/** 记录登录信息 : */@Overridepublic void recordLoginInfo(Long userId) {SysUser sysUser = new SysUser();sysUser.setUserId(userId);sysUser.setLoginIp(IpUtils.getIpAddr());sysUser.setLoginDate(DateUtils.getNowDate());sysUserService.updateUserProfile(sysUser);}/** 登录前置校验(对请求参数的校验) */private void loginCheck(String username , String password){ // 非空校验if (Objects.isNull(username)||Objects.isNull(password)){throw new UserNotExistsException() ;} // 长度校验// 密码如果不在指定范围内 错误if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH){throw new UserPasswordNotMatchException();}//用户名如果不在指定范围内if (username.length()<UserConstants.USERNAME_MIN_LENGTH||username.length()>UserConstants.USERNAME_MAX_LENGTH){throw new UserPasswordNotMatchException() ;}// IP黑名单校验//todo 这个地方调用selectConfigByKey这个方法,但是这个数据库里面根本没有记录黑名单的列String blackStr = sysConfigService.selectConfigByKey("sys.login.blackIPList");if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())){throw new BlackListException();}} }
③ 获取认证对象
我们需要调用spring security中的方法来获取一个认证对象,它会经过一系列的认证,只有最后用户的信息认证成功后才会生成一个认证对象,这说明用户认证成功,否则则说明这个用户认证失败。
④ 源码分析
那么我们就从这里作为入口,去看看springsecurity的源码是如何实现用户的账号信息以及密码校验的。
1.进入anthenticate方法后,我们发现这个是一个接口的抽象方法:
2.既然是接口,那我们就找到它的默认实现类ProviderManager,并且找到这个实现的方法:
3.那么我们继续看这个方法,这个方法的核心就是调用登录认证器的authenticate()方法:
4.我们现在知道这里主要就是调用了登录认证器的authenticate方法,那么我们就进这个方法看看里面实现了什么。最后发现这个方法也是一个接口的抽象方法:
5.然后我们找到它的实现类AbstractUserDetailsAuthenticationProvider,并且找到对应的方法:
6.我们找到实现方法authenticate后,发现它主要进行两步操作,先从缓存中获取到这个用户,如果是null,那么就调用etrieveUser()方法,而第一次登录里面肯什么都没有,所以是null,那么主要就是调用etrieveUser()方法:
7.那么不用多说,直接进入这个方法。但是发现它是一个抽象方法,那么我们肯定就是找到它的实现方法,去看里面的实现逻辑:
8.找到它的实现方法,这就已经到尾了,不过我们还记得一开时我们需要返回的值就是一个用户信息对象,所以这里也是找到了我们心心念念的 loadUserByUsername(username)方法:
9.那么我们肯定就进入这个方法去看看,发现它是接口 UserDetailsService的方法。
10.既然如此,我们只需要实现这个接口重写loadUserByUsername方法,然后从数据路根据用户名查询到用户信息,然后封装成一个loginUser对象,并且对这个用户的基本信息进行一个校验,没问题后就可以返回了,到这里根据用户名查找用户这个验证已经完成了。(不过需要注意,因为这个方法返回的是一个UserDetails,所以loginUser对象需要实现或者继承它才能作为该对象返回loginUser对象)
package com.fs.system.security;import cn.hutool.core.util.ObjectUtil; import com.fs.common.core.pojo.SysUser; import com.fs.common.core.vo.LoginUser; import com.fs.common.enums.UserStatus; import com.fs.common.exception.ServiceException; import com.fs.common.exception.user.UserNotExistsException; import com.fs.system.mapper.SysUserMapper; import com.fs.system.service.ipml.TokenService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; 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.Collections; import java.util.Objects;@Service public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate SysUserMapper userMapper ;@Autowiredprivate TokenService tokenService ;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {System.out.println("username:"+username); // 从数据库查询用户信息SysUser sysUser = userMapper.selectUserByUserName(username);System.out.println("查询到用户信息:"+sysUser);//验证用户是否存在if (Objects.isNull(sysUser)){System.out.println("用户不存在");throw new UserNotExistsException() ;} // 是否禁用if (sysUser.getStatus().equals(UserStatus.DISABLE)){throw new ServiceException("用户已封禁") ;} // 是否删除if (sysUser.getDelFlag()== UserStatus.DELETED.getCode()){throw new ServiceException("账号已经被删除");}// 创建LoginUser对象LoginUser loginUser = creatLoginUser(sysUser); // 创建tokentokenService.createToken(loginUser);return loginUser ;}public LoginUser creatLoginUser(SysUser sysUser){//先创建一个LoginUser对象,因为返回的的是LoginUser对象LoginUser loginUser = new LoginUser();loginUser.setUserId(sysUser.getUserId());loginUser.setUser(sysUser);System.out.println("成功查询user对象并且返回:"+loginUser);return loginUser ;} }
上面只是完成了用户名查到用户的功能,既然已经查询到用户的信息了,说明也就知道了用户的真实密码,那么我接下来就需要把用户输入的密码和真实密码做对比,所以我们继续回到DaoAuthenticationProvider类中。
在这里的 createSuccessAuthentication()方法中,我们成功看见了密码比对的方法:
我们直接找到源头,发现这个加密方法是一个PasswordEncoder接口的抽象方法,里面主要两个方法,分别用于对前端输入的密码加密,还有把加密后的密码和用户信息中的密码进行比对。
所以,我们要实现自定义的加密以及对密码进行比对,我们只需要实现这个接口,同时完成这个两个方法的实现就可以了:
package com.fs.system.security;import cn.hutool.core.convert.Convert; import com.fs.common.util.sign.PasswordUtils; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component;/*** @author suke* @version 1.0* @title MyPasswordEncoder* @description 自定义的密码编码* @create 2024/7/25 10:58*/ @Component public class MyPasswordEncoder implements PasswordEncoder {@Overridepublic String encode(CharSequence rawPassword) {return PasswordUtils.generate(Convert.toStr(rawPassword));}@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {return PasswordUtils.verify(Convert.toStr(rawPassword),encodedPassword);} }
PasswordUtils是我们用于密码的加密和比对的一个工具类,我们使用的是md5加密,工具类代码如下:package com.fs.common.util.sign;import java.security.MessageDigest; import java.util.Random;/*** MD5加盐加密*/ public class PasswordUtils {/*** 生成含有 随机盐的密码*/public static String generate(String password) {Random r = new Random();StringBuilder sb = new StringBuilder(16);sb.append(r.nextInt(99999999)).append(r.nextInt(99999999));int len = sb.length();if (len < 16) { //不够16位,前面补0for (int i = 0; i < 16 - len; i++) {sb.append("0");}}String salt = sb.toString();password = md5Hex(password + salt); //32位的16进制char[] cs = new char[48];for (int i = 0; i < 48; i += 3) {cs[i] = password.charAt(i / 3 * 2);char c = salt.charAt(i / 3);cs[i + 1] = c;cs[i + 2] = password.charAt(i / 3 * 2 + 1);}return new String(cs);}/*** 校验密码是否正确*/public static boolean verify(String password, String md5) {char[] cs1 = new char[32];char[] cs2 = new char[16];for (int i = 0; i < 48; i += 3) {cs1[i / 3 * 2] = md5.charAt(i);cs1[i / 3 * 2 + 1] = md5.charAt(i + 2);cs2[i / 3] = md5.charAt(i + 1);}String salt = new String(cs2);return md5Hex(password + salt).equals(new String(cs1));}/*** 获取十六进制字符串形式的MD5摘要*/public static String md5Hex(String src) {try {return Md5Utils.hash(src);} catch (Exception e) {return null;}}public static void main(String[] args) {System.out.println(generate("123456"));//System.out.println(verify("123456", "02dd65660724d38816641b3a98409fa2080401b489c9ff42"));} }
最后在这里,这个校验过程就完成了,最后会成功创建一个认证对象。
随后,我们只需要创建一个token,返回给前端就欧克了。
这里附上token创建的代码:
package com.fs.system.service.ipml;import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest;import cn.hutool.core.lang.UUID; import cn.hutool.core.util.StrUtil; import cn.hutool.http.useragent.UserAgent; import cn.hutool.http.useragent.UserAgentUtil; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.fs.common.constant.CacheConstants; import com.fs.common.constant.Constants; import com.fs.common.core.vo.LoginUser; import com.fs.common.util.RedisCache; import com.fs.common.util.ServletUtils; import com.fs.common.util.ip.AddressUtils; import com.fs.common.util.ip.IpUtils; import com.fs.system.config.TokenProperties; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service;/*** token验证处理*/ @Service public class TokenService {@Autowiredprivate TokenProperties tokenProperties;protected static final long MILLIS_SECOND = 1000;protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;@Autowiredprivate RedisCache redisCache;/*** 获取用户身份信息** @return 用户信息*/public LoginUser getLoginUser(HttpServletRequest request){// 获取请求携带的令牌String token = getToken(request);if (StringUtils.isNotEmpty(token)){try{Claims claims = parseToken(token);// 解析对应的权限以及用户信息String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);String userKey = getTokenKey(uuid);LoginUser user = redisCache.getCacheObject(userKey);return user;}catch (Exception e){}}return null;}/*** 设置用户身份信息*/public void setLoginUser(LoginUser loginUser){if (Objects.nonNull(loginUser) && StrUtil.isNotEmpty(loginUser.getToken())){refreshToken(loginUser);}}/*** 删除用户身份信息*/public void delLoginUser(String token){if (StringUtils.isNotEmpty(token)){String userKey = getTokenKey(token);redisCache.deleteObject(userKey);}}/*** 创建令牌** @param loginUser 用户信息* @return 令牌*/public String createToken(LoginUser loginUser){String token = UUID.randomUUID(false).toString(true);loginUser.setToken(token);setUserAgent(loginUser);refreshToken(loginUser);Map<String, Object> claims = new HashMap<>();claims.put(Constants.LOGIN_USER_KEY, token);return createToken(claims);}/*** 验证令牌有效期,相差不足20分钟,自动刷新缓存** @param loginUser* @return 令牌*/public void verifyToken(LoginUser loginUser){long expireTime = loginUser.getExpireTime();long currentTime = System.currentTimeMillis();if (expireTime - currentTime <= MILLIS_MINUTE_TEN){refreshToken(loginUser);}}/*** 刷新令牌有效期** @param loginUser 登录信息*/public void refreshToken(LoginUser loginUser){loginUser.setLoginTime(System.currentTimeMillis());loginUser.setExpireTime(loginUser.getLoginTime() + tokenProperties.getExpireTime() * MILLIS_MINUTE);// 根据uuid将loginUser缓存String userKey = getTokenKey(loginUser.getToken());redisCache.setCacheObject(userKey, loginUser, tokenProperties.getExpireTime(), TimeUnit.MINUTES);}/*** 设置用户代理信息** @param loginUser 登录信息*/public void setUserAgent(LoginUser loginUser){UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));String ip = IpUtils.getIpAddr();loginUser.setIpaddr(ip);loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));loginUser.setBrowser(userAgent.getBrowser().getName());loginUser.setOs(userAgent.getOs().getName());}/*** 从数据声明生成令牌** @param claims 数据声明* @return 令牌*/private String createToken(Map<String, Object> claims){String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, tokenProperties.getSecret()).compact();return token;}/*** 从令牌中获取数据声明** @param token 令牌* @return 数据声明*/private Claims parseToken(String token){return Jwts.parser().setSigningKey(tokenProperties.getSecret()).parseClaimsJws(token).getBody();}/*** 从令牌中获取用户名** @param token 令牌* @return 用户名*/public String getUsernameFromToken(String token){Claims claims = parseToken(token);return claims.getSubject();}/*** 获取请求token** @param request* @return token*/private String getToken(HttpServletRequest request){String token = request.getHeader(tokenProperties.getHeader());if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)){token = token.replace(Constants.TOKEN_PREFIX, "");}return token;}private String getTokenKey(String uuid){return CacheConstants.LOGIN_TOKEN_KEY + uuid;} }
最后再附上spring security配置类的完成代码:
package com.fs.system.config;import com.fs.common.util.RedisCache; import com.fs.system.security.JwtAuthenticationTokenFilter; import com.fs.system.security.MyDaoAuthenticationProvider; import com.fs.system.security.MyPasswordEncoder; import com.fs.system.security.TokenAuthenticationEntryPoint; 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.authentication.AuthenticationManager; import org.springframework.security.config.BeanIds; 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.builders.WebSecurity; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.filter.CorsFilter;/*** @author suke* @version 1.0* @title SpringSecurityConfiguration* @description springSecurity的自定义配置类* @create 2024/7/25 10:14*/ @Configuration public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {@Autowired // @Qualifier("userDetailsServiceImpl")private UserDetailsService userDetailsService;@Autowiredprivate RedisCache redisCache ;@Autowiredprivate SysPasswordProperties passwordProperties ;@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter ;@Autowiredprivate CorsFilter corsFilter;@Autowiredprivate TokenAuthenticationEntryPoint tokenAuthenticationEntryPoint;// @Autowired // private LogoutSuccessHandlerImpl logoutSuccessHandler;//对密码进行加密 密码校验@Beanpublic PasswordEncoder passwordEncoder(){return new MyPasswordEncoder() ;}//修改用户名,密码@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {System.out.println("进入配置类...");//使用MyDaoAuthenticationProviderauth.authenticationProvider(new MyDaoAuthenticationProvider(userDetailsService,redisCache,passwordEncoder(),passwordProperties));} 修改用户名,密码 //@Override //protected void configure(AuthenticationManagerBuilder auth) throws Exception { // //使用MyDaoAuthenticationProvider // auth.authenticationProvider(new MyDaoAuthenticationProvider()); //}@Bean(name = BeanIds.AUTHENTICATION_MANAGER)@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http// CSRF禁用,因为不使用session.csrf().disable()// 禁用HTTP响应标头.headers().cacheControl().disable().and()// 基于token,所以不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 过滤请求.authorizeRequests()// 对于登录login 注册register 验证码captchaImage 允许匿名访问.antMatchers("/login","/getRouters", "/captchaImage").permitAll()// 静态资源,可匿名访问.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll().antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated().and().headers().frameOptions().disable();//添加过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//添加跨域过滤器http.addFilterBefore(corsFilter,JwtAuthenticationTokenFilter.class);//配置认证失败的入口http.exceptionHandling().authenticationEntryPoint(tokenAuthenticationEntryPoint); // // //配置注销 // http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);}}