SpringSecurity认证

一、SpringSecurity简介

官方介绍:Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。

1、创建SpringSecurity项目

在以前创建springboot项目基础上加上security依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2、测试依赖是否导入成功

随便写一个controller接口,发出请求后出现以下验证页面就说明依赖第一个security项目创建成功。只有先登录验证之后才能继续访问接口,默认用户名是:user,密码在日志里面有输出,复制过来就行。
在这里插入图片描述
在这里插入图片描述

二、重写认证流程

1、导入对应依赖

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.23</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

2、配置数据库连接

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/test02
      username: root
      password: 1234

  redis:
    host: localhost
    port: 6379
    #    password: 1234
    database: 0
  cache:
    redis:
      time-to-live: 1800000

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:/mapper/*

3、导入工具类

  1. redis序列化类
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{
    
    

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static
    {
    
    
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class<T> clazz)
    {
    
    
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
    
    
        if (t == null)
        {
    
    
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
    
    
        if (bytes == null || bytes.length <= 0)
        {
    
    
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }


    protected JavaType getJavaType(Class<?> clazz)
    {
    
    
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}
  1. redis配置类
@Configuration
public 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;
    }
}
  1. JWT工具类
/**
 * JWT工具类
 */
public class JwtUtil {
    
    

    //有效期为
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "cx";

    public static String getUUID(){
    
    
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }
    
    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
    
    
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    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("cx")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }

    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    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);
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
    
    
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    
    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
    
    
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }


}
  1. 重写RedisTemplate
@SuppressWarnings(value = {
    
     "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    
    
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
    
    
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
    
    
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
    
    
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
    
    
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
    
    
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
    
    
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
    
    
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
    
    
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
    
    
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
    
    
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
    
    
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
    
    
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
    
    
        if (dataMap != null) {
    
    
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
    
    
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
    
    
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
    
    
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     * 
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
    
    
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
    
    
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
    
    
        return redisTemplate.keys(pattern);
    }
}

4、实现UserDetails 接口

SpringSecurity初始验证登录信息时,是通过它自己默认的用户名以及日志中生成的密码验证的。但是我们在项目中肯定不是使用它的用户信息,我们需要验证自己数据库中存在的用户信息。UserDetails是一个返回结果接口,里面封装了认证成功之后的用户信息,我们需要将信息替换成自己数据库对用用户信息,就需要实行这个接口。将用户实体作为成员变量,再通过lombok就可以实现用户信息封装。

/**
 * 实现UserDetails接口
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
    
    

    private User user;
    
    /**
     * 返回权限信息
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        return null;
    }

    @Override
    public String getPassword() {
    
    
        return user.getPassword();
    }

    @Override
    public String getUsername() {
    
    
        return user.getUserName();
    }


    /**
     * 判断用户是否没过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
    
    
        return true;
    }

    /**
     * 判断用户是否没有超时
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return true;
    }

    /**
     * 判断用户是否可用
     * @return
     */
    @Override
    public boolean isEnabled() {
    
    
        return true;
    }
}

5、实现UserDetailsService接口

UserDetailsService验证登录用户是否存在,是否合法等信息的,SpringSecurity初始认证不是认证我们数据库用户信息,所有需要重写此接口。接口返回结果类型是UserDetails ,上一步我们已经重写里面的方法,返回的结果就是用户验证成功后的信息。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
    
    

//        查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName, userName);
        User user = userMapper.selectOne(wrapper);

//        未查询到用户
        if(Objects.isNull(user)){
    
    
            throw new RuntimeException("用户名不存在");
        }

//        查询成功返回用户信息
        return new LoginUser(user);
    }
}

6、将BCryptPasswordEncoder注入到容器

BCryptPasswordEncoder类是一个加密解密类,只有将它注入到容器,替换掉SpringSecurity默认的密码验证机制PasswordEncoder ,密码验证才不需要说明验证机制。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    
    @Bean
    public PasswordEncoder passwordEncoder(){
    
    
        return new BCryptPasswordEncoder();
    }

}

三、实现登录接口

1、将AuthenticationManager注入到容器

AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?没想到吧),所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个List列表,存放多种认证方式。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }

2、登录接口放行

在系统中并不是所有的接口都需要先认证后访问的,登录接口不需要验证就应该能够被访问,需要将不用验证的接口配置好。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
    }

3、定义LoginService

@Service
public class LoginServiceImpl implements LoginService {
    
    

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public R login(User user) {
    
    
//        AuthenticationManager用户名密码认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

//        认证失败
        if(Objects.isNull(authenticate)){
    
    
            throw new RuntimeException("登录失败");
        }

//        认证成功
//        使用用户id,生成JWT
        LoginUser principal = (LoginUser) authenticate.getPrincipal();
        Long id = principal.getUser().getId();
        String jwt = JwtUtil.createJWT(id.toString());
        Map<String, String> map = new HashMap<>();
        map.put("token",jwt);

//        将用户信息存入redis
        redisCache.setCacheObject("login:"+ id, principal);

        return R.ok(map);
    }

}

4、定义LoginController

@RestController
@RequestMapping("/login")
public class LoginController {
    
    

    @Autowired
    private LoginService loginService;

    @PostMapping
    public R login(@RequestBody User user){
    
    
        return loginService.login(user);
    }
}

四、token认证

1、定义jwt认证过滤器


@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
    

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    
    
//        获取token
        String token = httpServletRequest.getHeader("token");
        if (!StringUtils.hasText(token)) {
    
    
//            放行
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
//        解析token
        String id;
        try {
    
    
            Claims claims = JwtUtil.parseJWT(token);
            id = claims.getSubject();
        } catch (Exception e) {
    
    
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
//        redis获取用户信息
        String redisKey = "userid:" + id;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
    
    
            throw new RuntimeException("用户未登录");
        }
//        存入securityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//        放行
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

2、配置jwt认证过滤器

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

五、退出登录

    @GetMapping("/logout")
    public R logout(){
    
    
//        从securityContextHolder获取用户id
        UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser principal = (LoginUser) authenticationToken.getPrincipal();
        Long id = principal.getUser().getId();
//        删除redis里面用户信息
        redisCache.deleteObject("login:"+id);

        return R.ok("注销成功");
    }

猜你喜欢

转载自blog.csdn.net/chenxingxingxing/article/details/125763953