SpringSecurity+JWT+redis认证流程


1. 整合SpringSecurity


1.1 工作流程

Spring Security的设计思想:通过一层层的Filters来对web请求做处理。

在这里插入图片描述

​ 如上图,一个请求想要访问到API就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。

大概security认证方案如下:图片来自

在这里插入图片描述

流程说明:

  1. 客户端发起一个请求,进入 Security 过滤器链。

  2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理。如果不是登出路径则直接进入下一个过滤器。

  3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler ,登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。

  4. 进入认证BasicAuthenticationFilter进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecurityContext的属性authentication上面。如果认证失败就会交给AuthenticationEntryPoint认证失败处理类,或者抛出异常被后续ExceptionTranslationFilter过滤器处理异常,如果是AuthenticationException就交给AuthenticationEntryPoint处理,如果是AccessDeniedException异常则交给AccessDeniedHandler处理。

  5. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。

1.2 SpringSecurity的重要概念
  • SecurityContext:上下文对象,Authentication对象会放在里面。

  • SecurityContextHolder:用于拿到上下文对象的静态工具类。

  • Authentication:认证接口,定义了认证对象的数据形式。

  • AuthenticationManager:用于校验Authentication,返回一个认证完成后的Authentication对象。


1. SecurityContext

上下文对象,认证后的数据就放在这里面,接口定义如下:

public interface SecurityContext extends Serializable {
 // 获取Authentication对象
 Authentication getAuthentication();

 // 放入Authentication对象
 void setAuthentication(Authentication authentication);
}
2. SecurityContextHolder
public class SecurityContextHolder {

 public static void clearContext() {
  strategy.clearContext();
 }

 public static SecurityContext getContext() {
  return strategy.getContext();
 }
    
    public static void setContext(SecurityContext context) {
  strategy.setContext(context);
 }

}

可以说是SecurityContext的工具类,通常用于get、set、clear SecurityContext,默认会把数据都存储到当前线程中。

3. Authentication
public interface Authentication extends Principal, Serializable {
 
	Collection<? extends GrantedAuthority> getAuthorities();
 	Object getCredentials();
 	Object getDetails();
 	Object getPrincipal();
	boolean isAuthenticated();
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

这几个方法效果如下:

  • getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息

  • getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。

  • getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)。

  • getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails。

  • isAuthenticated: 获取当前 Authentication 是否已认证。

  • setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。

Authentication只是定义了一种在SpringSecurity进行认证过的数据的数据形式应该是怎么样的,要有权限,要有密码,要有身份信息,要有额外信息。

4. AuthenticationManager
public interface AuthenticationManager {
 // 认证方法
 Authentication authenticate(Authentication authentication)
   throws AuthenticationException;
}

AuthenticationManager定义了一个认证方法,它将一个未认证的Authentication传入,返回一个已认证的Authentication,默认使用的实现类为:ProviderManager。

如此,将这四个部分串联起来,构成Spring Security进行认证的流程:

  1. 先是一个请求带着身份信息进来

  2. 经过AuthenticationManager的认证,

  3. 再通过SecurityContextHolder获取SecurityContext

  4. 最后将认证后的信息放入到SecurityContext

1.3 引入Security与jwt
  • pom.xml
<!-- spring-boot-security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--jwt-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

<!-- hutool工具类-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.3</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.11</version>
</dependency>

2. 准备工作


数据库操作框架采用的是mybatis-plus

2.1 重写UserDetailsService

我们需要重新定义查用户数据的过程,重写UserDetailsService接口。

  • com.hdu.back.dto.LoginUser

    UserDetails 是一个定义了数据形式的接口,用于保存我们从数据库中查出来的数据,其功能主要是验证账号状态和获取权限。

    将用户信息和权限信息组装成一个UserDetails对象,重写UserDetails对象:

/**
 * 登录用户身份权限
 *
 */
public class LoginUser extends User implements UserDetails {
    /**
     * 用户唯一标识
     */
    private String token;

    /**
     * 登录时间
     */
    private Long loginTime;

    /**
     * 过期时间
     */
    private Long expireTime;

    /**
     * 权限列表
     */
    private List<Permission> permissions;

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }


    public List<Permission> getPermissions() {
        return permissions;
    }

    public void setPermissions(List<Permission> permissions) {
        this.permissions = permissions;
    }


    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return permissions.parallelStream().filter(p -> !StringUtils.isEmpty(p.getPermission()))
                .map(p -> new SimpleGrantedAuthority(p.getPermission())).collect(Collectors.toSet());
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        // do nothing
    }

    // 账户是否未过期
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 账户是否未锁定
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 密码是否未过期
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 账户是否可用
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }

    public Long getLoginTime() {
        return loginTime;
    }

    public void setLoginTime(Long loginTime) {
        this.loginTime = loginTime;
    }

    public Long getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(Long expireTime) {
        this.expireTime = expireTime;
    }

}
  • com.hdu.back.service.impl.UserDetailsServiceImpl

security在认证用户身份的时候会调用UserDetailsService.loadUserByUsername()方法,根据用户名查询用户对象。

 /**
  * UserDetailsService
  * 用于在认证器中根据用户传过来的用户名查找一个用户
  */
 @Slf4j
 @Service
 public class UserDetailsServiceImpl implements UserDetailsService {
 
     @Autowired
     UserService userService;
 
     @Autowired
     PermissionService permissionService;
 
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         User user = userService.getOne(new QueryWrapper<User>().eq("username",username));
         if (user == null) {
             log.info("登录用户:{} 不存在.", username);
             throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
         }
         LoginUser loginUser = new LoginUser();
         BeanUtils.copyProperties(user, loginUser);
         List<Permission> permissions = permissionService.getPermissions(user.getId());
         loginUser.setPermissions(permissions);
         return loginUser;
     }
 }
2.2 Redis工具类
  • com.hdu.back.utils.RedisUtil
/**
 * redis操作工具类
 */
@Component
public final class RedisUtil {

    @Autowired
    @Qualifier("myRedisTemplate")
    private RedisTemplate<String, Object> redisTemplate;


    // =============================common============================

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
            }
        }
    }


    // ============================String=============================

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }


    // ================================Map=================================

    /**
     * HashGet
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 获取hashKey对应的所有键
     *
     * @param key 键
     * @return 对应的多个键
     */
    public Set hmgetKey(String key) {
        return redisTemplate.opsForHash().keys(key);
    }


    /**
     * 获取hashKey对应的所有值
     *
     * @param key 键
     * @return 对应的多个值
     */
    public Collection hmgetValue(String key) {
        return redisTemplate.opsForHash().values(key);
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除hash表中的值
     *  @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     * @return
     */
    public Long hdel(String key, Object... item) {
        return redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 删除hash表
     *  @param hash  键 不能为null
     * @return
     */
    public Long hdel(String hash) {
        return redisTemplate.opsForHash().delete(hash);
    }

    /**
     * 获得hash表的长度
     * @return
     */
    public int hGetHashSize(String hash){
        return redisTemplate.opsForHash().size(hash).intValue();
    }

    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }

    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }

    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     *
     * @param key 键
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */

    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */

    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }

}
2.3 ResponseUtil
  • com.hdu.back.utils.ResponseUtil
public class ResponseUtil {

   /**
    * 获取request
    */
   public static HttpServletRequest getRequest()
   {
      return getRequestAttributes().getRequest();
   }

   /**
    * 获取response
    */
   public static HttpServletResponse getResponse()
   {
      return getRequestAttributes().getResponse();
   }

   /**
    * 获取session
    */
   public static HttpSession getSession()
   {
      return getRequest().getSession();
   }

   public static ServletRequestAttributes getRequestAttributes()
   {
      RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
      return (ServletRequestAttributes) attributes;
   }

   /**
    * 将data渲染到客户端
    *
    * @param response
    * @param status
    * @param data
    */
   public static void responseJson(HttpServletResponse response, int status, Object data) {
      try {
         response.setHeader("Access-Control-Allow-Origin", "*");
         response.setHeader("Access-Control-Allow-Methods", "*");
         response.setContentType("application/json;charset=UTF-8");
         response.setStatus(status);

         response.getWriter().write(JSONObject.toJSONString(data));
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}
2.4 解决跨域问题
  • com.hdu.back.config.CorsConfig
/**
 * 解决跨域问题
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 设置访问源地址
        corsConfiguration.addAllowedOrigin("*");
        // 设置访问源请求头
        corsConfiguration.addAllowedHeader("*");
        // 设置访问源请求方法
        corsConfiguration.addAllowedMethod("*");
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 对接口配置跨域设置
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}
2.5 TokenService
  • application.yml

    添加token配置

# token配置
token:
  # 令牌自定义标识
  header: Authorization
  # 令牌密钥
  secret: V9RlaFCwZfxQ#PLJxRfKGU0IGNZKP7Kq
  # 令牌有效期(默认30分钟)
  expireTime: 1800
  • com.hdu.back.service.TokenService

    Token实现类

    基于JWT的认证模式,定义一个Token实现类,包括:创建Token刷新Token获取登录用户信息删除登录用户信息校验Token

/**
 * Token管理器
 * 可存储到redis
 * 基于redis
 */
public interface TokenService {

	Token saveToken(LoginUser loginUser);

	void refresh(LoginUser loginUser);

	LoginUser getLoginUser(HttpServletRequest request);

	void delLoginUser(String token);

	void verifyToken(LoginUser loginUser);

}
  • com.hdu.back.service.impl.TokenServiceImpl
@Component
public class TokenServiceImpl implements TokenService {
    // 令牌自定义标识
    @Value("${token.header}")
    private String header;

    // 令牌秘钥
    @Value("${token.secret}")
    private String secret;

    // 令牌有效期(默认1800秒)
    @Value("${token.expireTime}")
    private int expireTime;

    @Autowired
    private RedisUtil redisUtil;

    // 20分钟
    private static final Long MILLIS_MINUTE_TWENTY = 20 * 60 * 1000L;

    /**
     * 创建令牌
     *
     * @param loginUser 用户信息
     * @return 令牌
     */
    @Override
    public Token saveToken(LoginUser loginUser) {
        String token = UUID.randomUUID().toString();
        loginUser.setToken(token);
        cacheLoginUser(loginUser);

        String jwtToken = createJwtToken(loginUser);

        return new Token(jwtToken, loginUser.getLoginTime());
    }

    /**
     * 生成jwt
     *
     * @param loginUser
     * @return
     */
    private String createJwtToken(LoginUser loginUser) {
        Map<String, Object> claims = new HashMap<>();
        // 放入一个随机字符串,通过该串可找到登陆用户
        claims.put(Constants.LOGIN_USER_KEY, loginUser.getToken());

        String jwtToken = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
        return jwtToken;
    }

    /**
     * 缓存登录信息
     *
     * @param loginUser 登录信息
     */
    private void cacheLoginUser(LoginUser loginUser) {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * 1000);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisUtil.set(userKey,loginUser,expireTime);
    }

    private String getTokenKey(String uuid) {
        return Constants.LOGIN_TOKEN_KEY + uuid;
    }

    /**
     * 更新缓存的用户信息
     */
    @Override
    public void refresh(LoginUser loginUser) {
        cacheLoginUser(loginUser);
    }

    /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    @Override
    public LoginUser getLoginUser(HttpServletRequest request) {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token)) {
            Claims claims = parseToken(token);
            // 解析对应的权限以及用户信息
            String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
            String userKey = getTokenKey(uuid);
            LoginUser user = (LoginUser) redisUtil.get(userKey);
            return user;
        }
        return null;
    }

    /**
     * 获取请求token
     *
     * @param request
     * @return token
     */
    private String getToken(HttpServletRequest request) {
        String token = request.getHeader(header);
        if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 设置用户身份信息
     */
    public void setLoginUser(LoginUser loginUser) {
        if (loginUser != null && StringUtils.isNotEmpty(loginUser.getToken())) {
            refresh(loginUser);
        }
    }

    /**
     * 删除用户身份信息
     */
    @Override
    public void delLoginUser(String token) {
        if (StringUtils.isNotEmpty(token)) {
            String userKey = getTokenKey(token);
            redisUtil.del(userKey);
        }
    }

    /**
     * 验证令牌有效期,相差不足20分钟,自动刷新缓存
     *
     * @param loginUser
     * @return 令牌
     */
    @Override
    public void verifyToken(LoginUser loginUser){
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY){
            refresh(loginUser);
        }
    }


}

3. 具体实现流程


3.1 Security配置
  • com.hdu.back.config.SecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    AuthenticationSuccessHandler loginSuccessHandler;

    @Autowired
    AuthenticationFailureHandler loginFailureHandler;

    @Autowired
    LogoutSuccessHandler logoutSussHandler;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    UserDetailsService userDetailsService;

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    // 密码加密和验证策略
    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 定义白名单
    public static final String[] URL_WHITELIST = {

            "/webjars/**",
            "/favicon.ico",
            "/captcha",
            "/login",
            "/logout",
            "/*.html",
            "/**/*.html",
            "/**/*.css",
            "/swagger-ui.html",
            "/swagger-resources/**",
            "/v2/**",
            "/**/*.js"
    };

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF禁用,因为不使用session
        http.csrf().disable()
                // 解决不允许显示在iframe的问题
                .headers().frameOptions().disable()

                // 登录配置
                .and()
                .formLogin()
                .failureHandler(loginFailureHandler)
                .successHandler(loginSuccessHandler)

                // 退出配置
                .and()
                .logout()
                .logoutSuccessHandler(logoutSussHandler)

                // 配置拦截规则
                .and()
                .authorizeRequests()
                .antMatchers(URL_WHITELIST).permitAll() //白名单
                .anyRequest().authenticated()   // 除白名单外的所有请求全部需要鉴权认证

                // 异常处理器
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)  // 认证失败处理类

                // 基于token,所以不需要session
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                // 配置自定义的过滤器
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }


    /**
     * AuthenticationManager(认证管理器) 的建造器
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 把UserDetailsServiceImpl配置到SecurityConfig中
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

配置说明:

  • configure(AuthenticationManagerBuilder auth)

    AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 会让Security 自动构建一个 AuthenticationManager;如果想要使用该功能,你需要配置一个 UserDetailServicePasswordEncoder

    UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户, PasswordEncoder 用于密码的加密与比对,存储用户密码的时候用PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。这里采用spring security中的BCryptPasswordEncoder加密算法,该算法采用SHA-256 +随机盐+密钥对密码进行加密,SHA系列是Hash算法,过程是不可逆的。

    如果重写了该方法,Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用UserDetailsService.loadUserByUsername ,然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功则返回一个 Authentication 对象。

  • configure(HttpSecurity http)

    见代码注释。

  • 自定义 AuthenticationManager

    解决无法直接注入 AuthenticationManager问题。

3.2 身份认证
1. 登录认证

首先,通过登录页面提交表单,由UsernamePasswordAuthenticationFilter过滤器进行登陆操作处理。

  • 通过attemptAuthentication 方法搜集参数(username、password),封装为验证请求UsernamePasswordAuthenticationToken,然后调用AuthenticationManager进行身份认证。
  • AuthenticationManager调用它的authenticate方法进行认证,该方法会去调用UserDetailsServiceImpl.loadUserByUsername,然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功则返回一个认证完成的Authentication对象。

登录成功之后会走AuthenticationSuccessHandler,登陆失败走AuthenticationFailureHandler。自定义登录成功操作类和登陆失败类:

  • com.hdu.back.security.SecurityHandler
@Configuration
@Slf4j
public class SecurityHandler {

    @Autowired
    TokenService tokenService;

    /**
     * 登陆成功,返回Token
     *
     */
    @Bean
    public AuthenticationSuccessHandler loginSuccessHandler() {
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                LoginUser loginUser = (LoginUser) authentication.getPrincipal();
                Token token = tokenService.saveToken(loginUser);
                ResponseUtil.responseJson(response, HttpStatus.OK.value(), token);
            }
        };
    }

    /**
     * 登陆失败
     *
     * @return
     */
    @Bean
    public AuthenticationFailureHandler loginFailureHandler() {
        return new AuthenticationFailureHandler() {

            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                                AuthenticationException exception) throws IOException, ServletException {
                String msg = null;
                if (exception instanceof BadCredentialsException) {
                    msg = "用户名或密码错误";
                } else {
                    msg = exception.getMessage();
                }
                ServerResponse info = new ServerResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg);
                ResponseUtil.responseJson(response, HttpStatus.OK.value(), info);
            }
        };
    }
}

登陆成功,authentication.getPrincipal()获取当前登陆用户信息,创建jwt令牌,然后将loginUser存入redis中,并返回token给前端;登录失败,返回错误给前端。

2. jwt过滤器

前端获得token之后,放在请求头上

  • src/utils/request.js
import axios from 'axios'
import {Notification, Message, MessageBox} from 'element-ui'
import {getToken} from '@/utils/auth'
import errorCode from '@/utils/errorCode'

axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
    // axios中请求配置有baseURL选项,表示请求URL公共部分
    // baseURL: process.env.VUE_APP_BASE_API,
    baseURL: '/api',
    // 超时
    timeout: 10000
})

// request拦截器
service.interceptors.request.use(config => {
    const token = getToken();
    if (token) {
        config.headers['Authorization'] = 'Bearer ' + token // 让每个请求携带自定义token 请根据实际情况自行修改
    }
    return config;
}, error => {
    console.log(error)
})

// 响应拦截器
service.interceptors.response.use(res => {
        // 未设置状态码则默认成功状态
        const code = res.data.status || 200;
        // 获取错误信息
        const msg = errorCode[code] || res.data.msg || errorCode['default']
        if (code === 401) {
            Message({
                message: msg,
                type: "error"
            })
        } else if (code === 500) {
            Message({
                message: msg,
                type: 'error'
            })
            return Promise.reject(new Error(msg))
        } else if (code !== 200) {
            Notification.error({
                title: msg
            })
            return Promise.reject('error')
        } else {
            return res.data
        }
    },
    error => {
        console.log('err' + error)
        let {message} = error;
        if (message === "Network Error") {
            message = "后端接口连接异常";
        } else if (message.includes("timeout")) {
            message = "系统接口请求超时";
        } else if (message.includes("Request failed with status code")) {
            message = "系统接口" + message.substr(message.length - 3) + "异常";
        }
        Message({
            message: message,
            type: 'error',
            duration: 5 * 1000
        })
        return Promise.reject(error)
    })

export default service

后端需要将jwt过滤器放在过滤器链中,用于解析token,因为我们没有session,所以我们每次去辨别这是哪个用户的请求的时候,都是根据请求中的token来解析出来当前是哪个用户。

所以我们新建一个JwtAuthenticationTokenFilter

  • com.hdu.back.security.JwtAuthenticationTokenFilter
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 获取当前登录用户信息
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (loginUser != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            // 校验token是否过期
            tokenService.verifyToken(loginUser);

            // 组装authentication对象
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
//            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // 将authentication信息放入到上下文对象中
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }

}

过程说明:

  • 拿到request头部对应的token信息,解析jwtToken,根据userKey从resdis中获取到用户信息。

  • 查看获取到的logUser是否为null,UserDetail是否保存于上下文中,以及查看token是否过期,。

  • 组装一个authentication对象,把它放在上下文对象中,这样后面的过滤器看到我们上下文对象中有authentication对象,就相当于我们已经认证过了。

3.3 认证失败和退出成功处理类
  • com.hdu.back.security.SecurityHandler
@Configuration
@Slf4j
public class SecurityHandler {

    @Autowired
    TokenService tokenService;

    /**
     * 认证失败处理类 返回未授权401
     *
     * @return
     */
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new AuthenticationEntryPoint() {

            @Override
            public void commence(HttpServletRequest request, HttpServletResponse response,
                                 AuthenticationException authException) throws IOException, ServletException {
                ServerResponse info = new ServerResponse(HttpStatus.UNAUTHORIZED.value(), "认证失败,无法访问系统资源,请重新登陆!");
                ResponseUtil.responseJson(response, HttpStatus.UNAUTHORIZED.value(), info);
            }
        };
    }

    /**
     * 退出处理
     *
     * @return
     */
    @Bean
    public LogoutSuccessHandler logoutSussHandler() {
        return new LogoutSuccessHandler() {

            @Override
            public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
                LoginUser loginUser = tokenService.getLoginUser(request);
                if (loginUser != null) {
                    // 删除用户缓存记录
                    tokenService.delLoginUser(loginUser.getToken());
                }
                ServerResponse info = new ServerResponse(HttpStatus.OK.value(), "退出成功");
                ResponseUtil.responseJson(response, HttpStatus.OK.value(), info);
            }
        };

    }
}

参考:

  • 8
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Boot 是一个用于构建微服务的开源框架,它能够快速搭建项目并且提供了许多便捷的功能和特性。Spring Security 是一个用于处理认证和授权的框架,可以保护我们的应用程序免受恶意攻击。JWT(JSON Web Token)是一种用于身份验证的开放标准,可以被用于安全地传输信息。Spring MVC 是一个用于构建 Web 应用程序的框架,它能够处理 HTTP 请求和响应。MyBatis 是一个用于操作数据库的框架,可以简化数据库操作和提高效率。Redis 是一种高性能的键值存储系统,可以用于缓存与数据存储。 基于这些技术,可以搭建一个商城项目。Spring Boot 可以用于构建商城项目的后端服务,Spring Security 可以确保用户信息的安全性,JWT 可以用于用户的身份验证,Spring MVC 可以处理前端请求,MyBatis 可以操作数据库,Redis 可以用于缓存用户信息和商品信息。 商城项目的后端可以使用 Spring BootSpring Security 来搭建,通过 JWT 来处理用户的身份验证和授权。数据库操作可以使用 MyBatis 来简化与提高效率,同时可以利用 Redis 来缓存一些常用的数据和信息,提升系统的性能。前端请求则可以通过 Spring MVC 来处理,实现商城项目的整体功能。 综上所述,借助于 Spring BootSpring SecurityJWTSpring MVC、MyBatis 和 Redis 这些技术,可以构建出一个高性能、安全可靠的商城项目,为用户提供良好的购物体验。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值