Spring Security Authentication

1. Introduction to Spring Security

Official introduction: Spring Security, which is a security framework based on Spring AOP and Servlet filters. It provides a comprehensive security solution, handling authentication and authorization at both the Web request level and the method call level.

1. Create a Spring Security project

Add security dependencies based on the previously created springboot project:

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

2. Test whether the dependency is imported successfully

Write a controller interface casually, and the following verification page will appear after sending the request, indicating that the creation of the first security project depends on success. You can continue to access the interface only after logging in and verifying. The default user name is: user, and the password is output in the log, just copy it.
insert image description here
insert image description here

2. Rewrite the authentication process

1. Import corresponding dependencies

        <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. Configure database connection

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. Import tools

  1. redis serialization class
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 class
@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 tool class
/**
 * 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. Rewrite 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. Implement the UserDetails interface

When Spring Security initially authenticates login information, it authenticates with its own default username and the password generated in the log. But we are definitely not using its user information in the project, we need to verify the user information that exists in our database. UserDetails is an interface for returning results, which encapsulates user information after successful authentication. We need to replace the information with user information for our own database, so we need to implement this interface. Use the user entity as a member variable, and then use lombok to realize user information encapsulation.

/**
 * 实现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. Implement the UserDetailsService interface

UserDetailsService verifies whether the login user exists, whether it is legal and other information. The initial authentication of Spring Security is not to authenticate our database user information, so this interface needs to be rewritten. The interface returns the result type is UserDetails. We have rewritten the method in the previous step, and the returned result is the information after the user verification is successful.

@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. Inject BCryptPasswordEncoder into the container

The BCryptPasswordEncoder class is an encryption and decryption class. Only by injecting it into the container and replacing Spring Security's default password verification mechanism PasswordEncoder, the password verification does not need to explain the verification mechanism.

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

}

3. Realize the login interface

1. Inject AuthenticationManager into the container

AuthenticationManager(Interface) is the core interface related to authentication, and it is also the starting point for initiating authentication, because in actual needs, we may allow users to log in with username + password, and at the same time allow users to log in with email + password, mobile phone number + password, or even, Users may be allowed to use fingerprints to log in (there is such an operation? Didn’t expect it), so it is AuthenticationManagergenerally not directly authenticated, and AuthenticationManagerthe common implementation class of the interface ProviderManagerwill maintain a List internally to store multiple authentication methods. That is to say, there is always only one core authentication entry: , and there are three AuthenticationManagerdifferent authentication methods: username + password ( ), email + password, and mobile phone number + password login .UsernamePasswordAuthenticationTokenAuthenticationProvider

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

2. Login interface release

Not all interfaces in the system need to be authenticated before accessing. The login interface should be accessible without authentication, and the interfaces that do not need authentication need to be configured.

    @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. Define 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. Define LoginController

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

    @Autowired
    private LoginService loginService;

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

4. Token authentication

1. Define the jwt authentication filter


@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. Configure jwt authentication filter

    @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);
    }

5. Log out

    @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("注销成功");
    }

Guess you like

Origin blog.csdn.net/chenxingxingxing/article/details/125763953