一、Spring Security内部认证流程
-
用户首次登录提交用户名和密码后spring security 的UsernamePasswordAuthenticationFilter把用户名密码封装Authentication对象
-
然后内部调用ProvideManager的authenticate方法进行认证,然后ProvideManager进一步通过内部调用DaoAuthencationPriovider的authenticate方法进行认证
-
DaoAuthencationPriovider通过调用UserDetailsService的实现类InMemoryDetialManager的loadUserByUsername方法查询用户,并把查询到的用户信息封装成一个UserDetails对象。注意InMemoryDetialManage是从内存中查询用户信息的,所以我们要定制化实现UserDetailsService接口来达到从数据库查询用户信息。
-
通过PasswordEncode对比返回的UserDetails对象的密码和Authentication对象的密码是否一致,如果密码正确就把UserDetails中的权限信息设置到Authentication对象中
-
如果成功返回Authentication对象就使用SecurityContextHolder.getContext().setAuthentication方法存储改对象,然后其他过滤器就会通过SecurityContextHolder来获取当前用户信息。
二、自定义认证流程
我们要定制化实现UserDetailsService接口来达到从数据库查询用户信息,所以重新写一个UserDetailsServiceImpl实现类。然后认证通过使用用户id生成一个jwt(也就是token),然后用userId 作为key,用户信息作为value存入redis中。用户第一次登陆认证流程如下图。
导入依赖
<!--SpringSecurity启动器--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--redis依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--fastjson依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> <!--jwt依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency>
UserDetailsServiceImpl代码
@Servicepublic class UserDetailServiceImpl implements UserDetailsService { @Resource private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根据用户名查询用户信息 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserName,username); User user = userMapper.selectOne(queryWrapper); //判断是否查到用户 如果没查到抛出异常 if(Objects.isNull(user)){ throw new RuntimeException("用户名不存在"); } //返回用户信息 // TODO 查询权限信息封装 return new LoginUser(user); }}
BlogLoginServiceImpl 登陆认证代码
@Servicepublic class BlogLoginServiceImpl implements BlogLoginService { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public ResponseResult login(User user) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); //判断是否认证通过 if(Objects.isNull(authenticate)){ throw new RuntimeException("用户名或密码错误"); } //获取userid 生成token LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userId = loginUser.getUser().getId().toString(); String jwt = JwtUtil.createJWT(userId); //把用户信息存入redis redisCache.setCacheObject("bloglogin:"+userId,loginUser); //把token和userinfo封装 返回 UserInfoVo userInfoVo = BeanCopyUtil.copyBean(loginUser.getUser(), UserInfoVo.class); BlogUserLoginVo blogUserLoginVo = new BlogUserLoginVo(jwt, userInfoVo); //把User转换成UserInfoVo return ResponseResult.okResult(blogUserLoginVo); }}
SecurityConfig配置类
@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/login").anonymous() // 除上面外的所有请求全部不需要认证即可访问 .anyRequest().permitAll(); http.logout().disable(); //允许跨域 http.cors(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }}
JwtUtil工具类
public class JwtUtil { //有效期为 public static final Long JWT_TTL = 24*60 * 60 *1000L;// 60 * 60 *1000 一个小时 //设置秘钥明文 public static final String JWT_KEY = "hhh"; public static String getUUID(){ String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间 return builder.compact(); } public static String createJWT(String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间 return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("sg") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate); } public static String createJWT(String id, String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间 return builder.compact(); } public static void main(String[] args) throws Exception { String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg"; Claims claims = parseJWT(token); System.out.println(claims); } public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); }}
redis缓存配置类
@Configurationpublic class RedisConfig { @Bean @SuppressWarnings(value = { "unchecked", "rawtypes" }) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class); // 使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); // Hash的key也采用StringRedisSerializer的序列化方式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; }}
下次用户再次发请求时带token(相对于没有前后端分离时的session)来进行访问,如果没有携带则不让访问,如果有就经过一个jwt认证过滤器来获取token,解析token,最后获取userId,封装成Authentication对象存入SecurityContextHolder和redis中以便其他过滤器或者资源来获取当前用户请求信息
最后感悟:springboot约定大于配置,许多接口都可以自定义,达到定制化功能,这也体现了springboot的强大,还有许多功能还没完成,授权功能和流程将在后续完成,敬请期待。