前言

Demo源码地址:https://gitee.com/ruozxby/springsecuritydemo

springSesurity本质是一串过滤器执行链 我们自定义主要分为三个部分 1.配置文件 2登录认证 3权限认证

大概流程 调用自定义登录—> 成功返回token(同时用户数据存入redis) —>后续调用则进入自定义的jwt校验过滤器,成功则把用户数据、权限数据存入SecurityContextHolder的Context中,后续过滤器会通过context中是否有数据判断是否登录成功—>进入权限认证—>权限认证成功进入接口

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请

求。

ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

FilterSecurityInterceptor:负责权限校验的过滤器。

1 配置文件

主要配置需要放行的接口,登录认证失败处理器,权限认证失败处理器,还有些其他配置(权限认证配置,密码编辑器等)

配置文件

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解
public class SecurityConfig {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private SpringSecurityUserDetailsConfig springSecurityUserDetailsConfig;

    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPointImpl;

    @Autowired
    private MyAccessDeniedHandlerImpl accessDeniedHandlerImpl;

    public JwtAuthenticationFilter authenticationJwtTokenFilter() {
        return new JwtAuthenticationFilter(redisTemplate);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        // 提供自定义loadUserByUsername
        authProvider.setUserDetailsService(springSecurityUserDetailsConfig);
        // 指定密码编辑器
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 禁用basic明文验证
                //.httpBasic().disable()
                // 禁用默认登录页
                //.formLogin().disable()
                // 禁用默认登出页
                //.logout().disable()
                // 前后端分离是无状态的,不需要session了,直接禁用。
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests((authorizeRequests -> {
                    authorizeRequests
                            // 允许直接访问授权登录接口
                            // .requestMatchers(HttpMethod.POST,"/user/login").permitAll()//当前接口无论登没登录都可以访问
                            .requestMatchers(HttpMethod.POST, "/user/login").anonymous()//当前接口未登录才能访问
                            //.requestMatchers(HttpMethod.POST, "/user/login").hasAnyAuthority("权限表示符")//当前接口必须要有某个权限才能访问
                            // 除上面外的所有请求全部需要鉴权认证
                            .anyRequest().authenticated();//其他所有接口登录才能访问
                }))
                //自定义 authenticationJwtTokenFilter过滤器 把用户数据存入 SecurityContextHolder中,方便后面的过滤器判断是否登录
                .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                // 设置自定义异常处理器
                .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(authenticationEntryPointImpl)) //认证失败处理器
                .exceptionHandling(exceptions -> exceptions.accessDeniedHandler(accessDeniedHandlerImpl)) //授权失败处理器
                .authenticationProvider(authenticationProvider());
        return http.build();

    }
}

登录认证失败处理器

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        //从本地线程获取errMsg
        Object errMsg = ThreadLocalUtil.getLaterRemove("errMsg");

        response.setStatus(HttpStatus.OK.value());
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        ResponseResult result= new ResponseResult(HttpStatus.FORBIDDEN.value(), Objects.isNull(errMsg) ? "登录认证失败,请重新登录" : errMsg);
        response.getWriter().print(JSON.toJSONString(result));
    }
}

权限认证失败处理器

@Component
public class MyAccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //获取失败信息

        //返回
        response.setStatus(HttpStatus.OK.value());
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        ResponseResult result= new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"权限不足"); //accessDeniedException.getMessage();
        response.getWriter().print(JSON.toJSONString(result));
    }
}

工具类

JWTUtil

public class JWTUtil {
    //加密算法
    private final static SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;

    //私钥 / 生成签名的时候使用的秘钥secret,一般可以从本地配置文件中读取,切记这个秘钥不能外露,只在服务端使用,在任何场景都不应该流露出去。
    // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
    private final static String secret = "secretKey";

    // 过期时间(单位秒)/ 2小时
    private final static Long access_token_expiration = 7200L;

    //jwt签发者
    private final static String jwt_iss = "jlLiu";

    //jwt所有人
    private final static String subject = "Liujl";


    /**
     * 创建jwt
     *
     * @return 返回生成的jwt token
     */
    public static String generateJwtToken(Map<String, Object> claims) {

        // 头部 map / Jwt的头部承载,第一部分
        // 可不设置 默认格式是{"alg":"HS256"}
        Map<String, Object> map = new HashMap<>();
        map.put("alg", "HS256");
        map.put("typ", "JWT");


        //载荷 map / Jwt的载荷,第二部分
/*        Map<String,Object> claims = new HashMap<String,Object>();

        //私有声明 / 自定义数据,根据业务需要添加
        claims.put("id","123456");
        claims.put("userName", "admin");*/

        //标准中注册的声明 (建议但不强制使用)
        //一旦写标准声明赋值之后,就会覆盖了那些标准的声明
        claims.put("iss", jwt_iss);
            /*  iss: jwt签发者
                sub: jwt所面向的用户
                aud: 接收jwt的一方
                exp: jwt的过期时间,这个过期时间必须要大于签发时间
                nbf: 定义在什么时间之前,该jwt都是不可用的.
                iat: jwt的签发时间
                jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
            */


        //下面就是在为payload添加各种标准声明和私有声明了
        return Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
                .setHeader(map)         // 头部信息
                .setClaims(claims)      // 载荷信息
                .setId(UUID.randomUUID().toString()) // 设置jti(JWT ID):是JWT的唯一标识,从而回避重放攻击。
                .setIssuedAt(new Date())       // 设置iat: jwt的签发时间
                //.setExpiration(new Date(System.currentTimeMillis() + access_token_expiration * 1000)) // 设置exp:jwt过期时间
                .setSubject(subject)    //设置sub:代表这个jwt所面向的用户,所有人
                .signWith(SIGNATURE_ALGORITHM, secret)//设置签名:通过签名算法和秘钥生成签名
                .compact(); // 开始压缩为xxxxx.yyyyy.zzzzz 格式的jwt token
    }

    public static String getUserIdByJwt(String jwt) {
        Claims claims = getClaimsFromJwt(jwt);
        return Objects.isNull(claims) ? null : Objects.toString(claims.get("userId"));
    }




    /**
     * 从jwt中获取 载荷 信息
     *
     * @param jwt
     * @return
     */
    private static Claims getClaimsFromJwt(String jwt) {
        Claims claims = null;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(jwt).getBody();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return claims;
    }

}

ThreadLocalUtil

public class ThreadLocalUtil {
    private static final ThreadLocal<Map<String, Object>> threadLocal = ThreadLocal.withInitial(() -> new HashMap<>(10));

    public static Map<String, Object> getThreadLocal() {
        return threadLocal.get();
    }

    public static Object get(String key) {
        Map<String, Object> map = threadLocal.get();
        return map.get(key);
    }

    public static void set(String key, Object value) {
        Map<String, Object> map = threadLocal.get();
        map.put(key, value);
    }

    public static void set(Map<String, Object> keyValueMap) {
        Map<String, Object> map = threadLocal.get();
        map.putAll(keyValueMap);
    }

    public static void remove() {
        threadLocal.remove();
    }

    public static <T> T remove(String key) {
        Map<String, Object> map = threadLocal.get();
        return (T) map.remove(key);
    }

    public static Object getLaterRemove(String key) {
        Map<String, Object> map = threadLocal.get();
        remove();
        return map.get(key);
    }
}

2 登录验证

登录验证主要分为两部分

​ 1 自定义用户校验:通过前端传入的账号密码校验用户是否合法

​ 2 自定义jwt过滤器:校验token是否合法,合法则把用户数据、权限数据放入SecurityContextHolder的Context中,后续过滤器链会通过Context中是否有用户数据判断登录校验是否通过

​ 3 登录接口: 在登录接口中调用AuthenticationManager的authenticate方法,最终他会调用我们自定义的用户校验,以及选择的密码加密方式校验账号密码,成功则生成token,存入redis后返回

1 自定义用户校验

实现UserDetailsService接口 ,重写 loadUserByUsername 方法

@Component
public class SpringSecurityUserDetailsConfig implements UserDetailsService {

    /**
     * 自定义查询用户
     *
     * @param username
     * @return
     * @throws
     */

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //从数据库查询用户信息 权限信息
        User user = null;
        if (username.equals("root")) {
            // $2a$10$XDrWFhZC4btSkQQ4hs0Yte0RyZFjtts0wh4swL4Rv11arnyVhJBPy 123456
            user = new User(1L, "root", "$2a$10$XDrWFhZC4btSkQQ4hs0Yte0RyZFjtts0wh4swL4Rv11arnyVhJBPy");
        }
        //有问题 UsernameNotFoundException
        if (user == null) {
            ThreadLocalUtil.set("errMsg","登录失败,当前账号不存在");
            throw new UsernameNotFoundException("");
        }

        //查询用户的权限信息
        LoginUser loginUser = new LoginUser();
        //权限list 正常从数据库查询 rbac模型 用户->角色->权限
        ArrayList<String> roles = new ArrayList<>(List.of("menu1", "menu1"));

        //封装成userDetails返回
        loginUser.setUser(user);
        loginUser.setPermissions(roles);
        return loginUser;
    }
}

LoginUser 实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    /**
     * 自定义权限list
     */
    private List<String> permissions;

    @JSONField(serialize = false) //不序列化,序列化redis会报错
    private List<SimpleGrantedAuthority> authority;

    /**
     * 权限list 框架从这个接口过去权限list
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //单例模式,减少重复调用
        if (Objects.isNull(authority)){
            authority = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        }
        return authority;
    }

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

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

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

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

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

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

user实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User  implements Serializable {
    // 序列化 ID,通常是一个唯一的 long 值,用于版本控制
    private static final long serialVersionUID = 1L;

    private Long id;

    private String userName;

    private String password;
}

2 自定义jwt过滤器

在过滤器中校验token合法性以及插入 用户数据、权限数据到context中

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {


    private RedisTemplate redisTemplate;

    public JwtAuthenticationFilter(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 自定义filter 用户登录则把用户数据存入SecurityContextHolder上下文中,没有则直接放行,后面过滤器会自行判断是否有数据(是否登录)
     *
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String jwt = getJwtFromRequest(request);
        //TODO token时效校验
        if (StringUtils.isNotBlank(jwt)) {
            //token存在,通过token获取userId,通过userId从redis中拿用户数据
            String userId = JWTUtil.getUserIdByJwt(jwt);
            LoginUser redisLoginUser = (LoginUser) redisTemplate.opsForValue().get("api:login:" + userId);
            Optional.ofNullable(redisLoginUser).ifPresent(loginUser -> {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
                //authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            });

        }
        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        // 从请求头或其他地方获取 JWT
/*        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            return token.substring(7);
        }
        return null;*/
        return request.getHeader("token");
    }

}

记得在配置文件中加入自定义的过滤器

//自定义 authenticationJwtTokenFilter过滤器 把用户数据存入 SecurityContextHolder中,方便后面的过滤器判断是否登录
.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)

3 登录接口

调用springSecurity AuthenticationManager.authenticate 方法进行登录校验 最终会调用到自定义的用户查询逻辑,成功生成token,存入reids后返回

//调用springSecurity AuthenticationManager.authenticate 方法进行登录校验 最终会调用到自定义的用户查询逻辑
Authentication authenticate;
try {
    AuthenticationManager  authenticationManager = authenticationConfiguration.getAuthenticationManager();
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
     authenticate = authenticationManager.authenticate(authenticationToken);
} catch (Exception e) {
    ThreadLocalUtil.set("errMsg",Objects.isNull(ThreadLocalUtil.get("errMsg")) ? "登录失败,账号或密码错误" : ThreadLocalUtil.get("errMsg"));
    throw new RuntimeException(e);
}

//校验是否认证通过
if (Objects.isNull(authenticate)){
    ThreadLocalUtil.set("errMsg","登录失败,账号或密码错误");
    throw new RuntimeException("登录失败");
}

//认证通过生产jwt,存入redis后返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
//创建jwt
String jwt = JWTUtil.generateJwtToken(new HashMap<>() {{put("userId", userId);}});
//存入redis
redisTemplate.opsForValue().set("api:login:"+userId, loginUser);
return jwt;

3 权限验证

权限验证分为两个部分

​ 1 配置文件开启权限校验

​ 2 在接口上添加权限校验注解:注解指定该接口所需权限,自定义jwt校验中,成功会把权限数据存入context中,spring会去拿出做比较

1开启权限校验

在配置文件上通过注解开启权限校验

@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解

2 在接口上添加权限校验注解

默认权限校验

hasAnyAuthority是spring提供的默认权限校验中的一种

@PreAuthorize("hasAnyAuthority('menu2')")

在UserDetails实现类的实体类中,有个getAuthorities方法,spring会调用该方法

private User user;

/**
 * 自定义权限list
 */
private List<String> permissions;

@JSONField(serialize = false) //不序列化,序列化redis会报错
private List<SimpleGrantedAuthority> authority;

/**
 * 权限list 框架从这个接口获取权限list
 * @return
 */
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    //单例模式,减少重复调用
    if (Objects.isNull(authority)){
        authority = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }
    return authority;
}

其中 hasAnyAuthority 是spring提供的校验方式,我们也可以自定义权限校验

自定义权限校验

自定义权限校验器

@Component("sspc")
public class SpringSecurityPermissionsConfig {


    public boolean myHasAnyAuthority(String authority){
        //获取用户权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser)authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        return permissions.contains(authority);
    }
}

接口上

//@PreAuthorize("hasAnyAuthority('menu1')")
@PreAuthorize("@sspc.myHasAnyAuthority('menu1')")

不语
4 声望1 粉丝