SpringSecurity结合Jwt实现定制化认证过滤器的详细步骤

上篇文章讲了SpringSecurity的详细使用,这篇文章来说说SpringSecurity与Jwt结合来定制化认证过滤器,以此来改变SpringSecurity的默认认证方式。

首先了解一下Jwt是什么:

JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。

JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。

 JWT 认证可以有效避免 CSRF 攻击,因为 JWT 一般是存在在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。

Jwt本质上就是一个加密工具,下图是Jwt数据格式的三个部分:

SpringSecurity结合Jwt实现定制化认证过滤器的详细步骤_第1张图片

  • Header : 描述 JWT 使用的加密算法以及 Token 的类型等。

  • Payload : 用来存放需要传递的数据(如:账号,过期时间 等)

  • Signature(签名) :组合Payload、Header 和一个密钥(Secret)来加密生成,默认是 (HMAC SHA256)生成。

JWT 加密三个部分后生成 xxxx.xxxx.xxxx 格式的字符串,三个部分之间以 .相隔

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Payload 也是 JSON 格式数据,其中包含了 Claims(声明,包含 JWT 的相关信息)。

Claims 分为三种类型(了解下面源码后能更好的理解三种类型):

Registered Claims(注册声明:默认)、Public Claims(公有声明:自定义)、Private Claims(私有声明:自定义)

Payload的注册声明的定义:

  • iss(issuer):JWT 签发方。(本文未用)

  • iat(issued at time):JWT 签发时间。(token生效时间)

  • sub(subject):JWT 主题。(本文用来存放账号)

  • aud(audience):JWT 接收方。(本文未用)

  • exp(expiration time):JWT 的过期时间。(token过期时间)

  • nbf(not before time):JWT 生效时间,早于该时间的 JWT 不能被接受处理。(本文未用)

  • jti(JWT ID):JWT 唯一标识。

引入依赖:

        
        
            org.springframework.boot
            spring-boot-starter-security
        
        
        
            io.jsonwebtoken
            jjwt
            0.9.1
        

Payload本质上是一个Map集合,上面的命名表示Map集合默认键名,我们通过put()来定义参数(可自行查看源码):

SpringSecurity结合Jwt实现定制化认证过滤器的详细步骤_第2张图片

SpringSecurity结合Jwt实现定制化认证过滤器的详细步骤_第3张图片

SpringSecurity结合Jwt实现定制化认证过滤器的详细步骤_第4张图片

注意:实现定制化认证过滤器只是认证,而授权还是交给Security框架实现。

下面来看看具体实现步骤(建议对照底部测试结果图来理解):

步骤1:

创建Jwt加密的工具类(拷贝即用):

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken生成的工具类
 * JWT token的格式:header.payload.signature
 * header的格式(算法、token的类型):
 * {"alg": "HS512","typ": "JWT"}
 * payload的格式(用户名、创建时间、生成时间):
 * {"sub":"wang","created":1489079981393,"exp":1489684781}
 * signature的生成算法:
 * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
 */
@Component
@Slf4j
public class JwtTokenUtil {
    private static final Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);

    @Value("${jwt.secret}")
    private String secret;// 密钥
    @Value("${jwt.expiration}")
    private Long expiration;  // 定义过期时间
    @Value("${jwt.tokenHead}")
    private String tokenHead;  // 自定义标识字符串

    /**
     * 根据用户信息生成token,其实只需传入账号,这里实际上只是将账号定义为了sub(主题)
     */
    public String generateToken(UserDetails userDetails) {
        logger.info("访问:JwtTokenUtil--generateToken()");
        String token = generateJwtToken(userDetails);
        return token;
    }
    /**
     * 刷新token,这里实际上只是解析token获取账号,然后重新生成token并返回
     */
    public String refreshToken(String token) {
        logger.info("访问:JwtTokenUtil--refreshToken()");
        Claims claims = getClaimsFromToken(token);
        User user = new User();
        user.setUsername(claims.getSubject());
        token = generateJwtToken(user);
        return token;
    }

    /**
     * 判断传入参数是否为null来判断是要生成token还是刷新token
     */
    private String generateJwtToken(UserDetails userDetails) {
        logger.info("访问:JwtTokenUtil--generateJwtToken()");
        // 构建Jwt的三个部分,使用的是设计模式里的建造者模式(Builder)
        JwtBuilder jwtBuilder = Jwts.builder();

        // Header部分:
        jwtBuilder.setHeaderParam("type","JWT"); // 仅作声明

        Map claims = new HashMap<>();
        jwtBuilder.setClaims(claims);  // claim: Playload部分,定义用当前容器来装载数据
        jwtBuilder.setSubject(userDetails.getUsername()); // sub:这里将账号设置为主题了。

        long iat = System.currentTimeMillis();
        jwtBuilder.setIssuedAt(new Date(iat)); // iat:签发的时间(token生成时间)
        jwtBuilder.setExpiration(new Date(iat + expiration)); // exp:过期时间

        // Signature部分:
        jwtBuilder.signWith(SignatureAlgorithm.HS512, secret); // Signature部分:指定算法进行签名,生成一个jws
        String token = jwtBuilder.compact(); //构建JWT并将其序列化为一个紧凑的、url安全的字符串(token)
        token = tokenHead +" "+ token;  // 自定义标识字符串 拼接 token值
        return token;
    }

    /**
     * 从token中获取JWT中的负载(解析token值)
     */
    private Claims getClaimsFromToken(String token) {
        logger.info("访问:JwtTokenUtil--getClaimsFromToken()");
        Claims claims = null;
        try {
            claims = Jwts.parser()   // 开始解析token
                    .setSigningKey(secret)  // 定义密钥(解密的盐)
                    .parseClaimsJws(token)  // 解析token,解析失败抛出异常
                    .getBody(); // 获取Payload中数据
        } catch (Exception e) {
            logger.info("JWT格式验证失败:{}",token);
        }
        logger.info(claims+"");
        return claims;
    }
    /**
     * 从token中获取过期时间
     */
    public Date getExpiredDateFromToken(String token) {
        logger.info("访问:JwtTokenUtil--getExpiredDateFromToken()");
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 从token中获取登录用户名()主题
     */
    public String getUserNameFromToken(String token) {
        logger.info("访问:JwtTokenUtil--getUserNameFromToken()");
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username =  claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }
    /**
     * 判断token是否已经失效
     */
    public boolean isTokenExpired(String token) {
        logger.info("访问:JwtTokenUtil--isTokenExpired()");
        if (StringUtils.isEmpty(token)){
            return false;
        }
        Date expiredDate = getExpiredDateFromToken(token);
        // Date判断过期时间是否在当前时间之前
        return expiredDate.before(new Date());
    }
}

步骤2:

GrantedAuthority:权限实体 

/**
 * 权限实体
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Authority implements GrantedAuthority, Serializable {
    private static final long serialVersionUID = 5534135L;
    private Integer id;
    private String authorityName;
    @Override
    public String getAuthority() {
        return authorityName;
    }
}

步骤3

UserDetails:用户实体(关联权限)

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails, Serializable {
    private static final long serialVersionUID = -3694902274397865672L;
    private Integer id;
    private String username;
    private String password;
    private ArrayList authorities;
    @Override
    public ArrayList getAuthorities() {
        return authorities;
    }
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}

步骤4:

UserDetailsService:用作返回过滤器的认证授权信息,传入的是username,这里主要是判断token中的用户名是否存在,不存在则返回null(即:认证失败),存在则表示认证成功,去获取用户名对应的所有权限,然后封装到UserDetails中并返回,剩下的授权操作就交给框架来完成。

@Service
public class AccountService implements UserDetailsService {
    private static final Logger logger = LoggerFactory.getLogger(AccountService.class);
    @Resource
    private UserAndAuthorityServiceImpl userAndAuthorityService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("访问:AccountService--loadUserByUsername()--值:"+username);
        User user = userAndAuthorityService.getUserAndAuthority(username);
        if (user==null){
            throw new ServiceException(ErrorInformation.SERVICE_ERROR);
        }
        return user;
    }
}

步骤5:

AccessDeniedHandler :过滤器检测到未登录时调用

// 过滤器检测到未登录时访问
@Component
public class NotLogInHandler implements AccessDeniedHandler {
    @Autowired
    ObjectMapper objectMapper;
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        LoggerFactory.getLogger(NotLogInHandler.class).info("访问:NotLogInHandler的handle()");
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json");
        String s = objectMapper.writeValueAsString(D.failed(null,"未登录不能访问!",null));
        PrintWriter writer = response.getWriter();
        writer.print(s);
        writer.flush();
    }
}

步骤6:

AuthenticationEntryPoint :过滤器检测到未授权时调用

// 过滤器检测到未授权时访问
@Component
public class UnauthorizedHandler implements AuthenticationEntryPoint {
    @Autowired
    ObjectMapper objectMapper;
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        LoggerFactory.getLogger(NotLogInHandler.class).info("访问:UnauthorizedHandler的commence()");
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json");
        String s = objectMapper.writeValueAsString(D.failed(null,"没有权限不能访问!",null));
        PrintWriter writer = response.getWriter();
        writer.print(s);
        writer.flush();
    }
}

步骤7:

OncePerRequestFilter :定制化过滤器,可以自定义认证规则

生成的token:

认证过滤器:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.httpHeader}")
    private String httpHeader;  // 请求头中token的键名

    @Value("${jwt.tokenHead}")
    private String tokenHead;   // xxx为token标记(如:"xxx token..."),token值要单独拆解出来

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {
        // 从header中获取Authorization
        String authHeader = request.getHeader(this.httpHeader);
        // 判断 authHeader  不为空  并且以 bearer 开头
        if (authHeader == null){
            filterChain.doFilter(request,response);
            return;
        }
        boolean isAuthizationHeader = StringUtils.startsWithIgnoreCase(authHeader,this.tokenHead);
        if (!isAuthizationHeader){
            filterChain.doFilter(request,response);
            return;
        }
        // 截取 bearer 后面的字符串,两端去空格(获取token)
        String authToken = authHeader.substring(this.tokenHead.length()).trim();
        // 验证token是否过期
        boolean tokenExpired = jwtTokenUtil.isTokenExpired(authToken);
        // 从token中获取登录用户名(主题)
        String username = jwtTokenUtil.getUserNameFromToken(authToken);
        if (tokenExpired && StringUtils.isNullOrEmpty(username)){
            filterChain.doFilter(request,response);
            return;
        }
        logger.info("checking username:{}", username);
        // 用户名不为空  并且SecurityContextHolder.getContext()  存储 权限的容器中没有相关权限则继续
        boolean isNotAuthentication = SecurityContextHolder.getContext().getAuthentication() == null;
        if (isNotAuthentication) {
            // 从数据库读取用户信息
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            if (userDetails != null) {
                // 将认证状态标记为已认证,一个带用户名和密码以及权限的Authentication(spring 自带的类)
                UsernamePasswordAuthenticationToken authentication = null;
                authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                // 从HttpServletRequest 对象,创建一个WebAuthenticationDetails对象
                WebAuthenticationDetails details = new WebAuthenticationDetailsSource().buildDetails(request);
                // 设置details
                authentication.setDetails(details);
                logger.info("authenticated user:{}", username);
                // 存入本线程的安全容器   在访问接口拿到返回值后 要去主动清除 权限,避免干扰其他的线程
                // SecurityContextHolder会把authentication放入到session里,供后面使用
                SecurityContextHolder.getContext().setAuthentication(authentication);
//                logger.info(jwtTokenUtil.);
                // 刷新token失效时间
                jwtTokenUtil.refreshToken(authToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

步骤8:

WebSecurityConfigurerAdapter :核心配置类,以上步骤完成后,必须将其配置到Security框架中,才能实现定制化认证过滤器。

@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private AccountService accountService;
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Resource
    private NotLogInHandler notLogInHandler;
    @Resource
    private UnauthorizedHandler unauthorizedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 配置控制器URL的角色和权限
                .antMatchers("/admin/user/login","/admin/room/upload","/download","/admin/user/loginout").permitAll() // 放行路径
                .anyRequest().authenticated()  // 表示除了放行路径,其它路径都要认证授权
                //Spring Security永远不会创建一个HttpSession,也永远不会使用它获取SecurityContext
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // 这一步,告诉Security 框架,我们要用自己的UserDetailsService实现类
                // 来传递UserDetails对象给框架,框架会把这些信息生成Authorization对象使用
                .and().userDetailsService(accountService);
        // 在UsernamePasswordAuthenticationFilter过滤前,我们使用jwt进行过滤
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

//添加自定义未授权和未登录结果返回
        http.exceptionHandling()
                .accessDeniedHandler(notLogInHandler)  // 未登录时访问
                .authenticationEntryPoint(unauthorizedHandler);  // 未授权时访问
        http.cors();
//        http.cors().disable();  // 关闭跨域, 默认开启
          http.csrf().disable();  // 关闭请求伪造防护,用session就不用关
    }

    // 加密方式:这个要有,框架要对密码做加密,不能是明文,
    // 也可以自定义加密方式实现PasswordEncoder接口再return自定义实现类
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

步骤9:

以上步骤只是实现了访问认证,下面是登陆校验的一种实现(仅作参考):

注意:要在上面配置类中配置放行路径。

@RestController
@RequestMapping("/admin/user")
public class UserAdmin{
    @Autowired
    private UserService userService;

    @RequestMapping("/login")
    public D login(
            User entity,
            HttpServletResponse response
    ){
        User user = userService.login(entity,response);
        return D.success(null,null,user);
    }
}
@Service
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ,timeout = 300,rollbackFor = Exception.class)
public class UserServiceImpl implements UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
    @Resource
    private UserDao mapper;
    @Resource
    private JwtTokenUtil jwtTokenUtil;
    @Resource
    private BCryptPasswordEncoder passwordEncoder;  // 加密解密
// 登陆验证
    @Override
    public User login(User entity, HttpServletResponse response){
        String rawPassword = entity.getPassword();
        // 从数据库中查询用户
        entity = mapper.selectOne(entity);
        // 判断账号是否存在
        if (entity == null) {
            throw new LoginException(ErrorInformation.MESSAGE_LOGIN_FAILED);
        }
        // 判断密码是否正确
        if (passwordEncoder.matches(rawPassword,entity.getPassword())){
            throw new LoginException(ErrorInformation.MESSAGE_LOGIN_FAILED);
        }
        String token = jwtTokenUtil.generateToken(entity);
        // 用户控制器带上token响应头,如不设置,前端无法接收响应头数据。
        response.setHeader("Access-Control-Expose-Headers", "token");
        // 设置token相应头
        response.setHeader("token", token);
        return entity;
    }
// 退出登陆
    @Override
    public void loginout(HttpServletResponse response) {
        response.setHeader("token",null);
    }

测试结果:成功获取token后,可将token值复制到Jwt官网解析

SpringSecurity结合Jwt实现定制化认证过滤器的详细步骤_第5张图片

Jwt官网链接:https://jwt.io

SpringSecurity结合Jwt实现定制化认证过滤器的详细步骤_第6张图片

你可能感兴趣的:(springboot,json,java)