Spring Boot 集成Spring Security实现JWT认证

492 阅读2分钟

前言

Spring Security默认采用用户名和密码认证方式,前后端分离项目中基于token进行认证,本文将讲解Spring Boot集成Spring Security实现jwt认证。

集成步骤

maven 依赖

   	<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		
     <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
		
	   <dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>${mybatis-plus.version}</version>
		</dependency>
		
	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
		<scope>runtime</scope>
	</dependency>
	
	<!--druid -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>${druid.version}</version>
		</dependency>
		
	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>fastjson</artifactId>
		<version>${fastjson.version}</version>
    </dependency>
    
    <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-security</artifactId>
   </dependency>
   
    <dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
  </dependency>
  
  	
	<dependency>
		<groupId>io.jsonwebtoken</groupId>
		<artifactId>jjwt</artifactId>
		<version>${jwt.version}</version>
	</dependency>

JWT工具类

    public static String createJWT(String subject, String issue, Object claim,
            long ttlMillis)
    {
        long nowMillis = System.currentTimeMillis();
        long expireMillis = nowMillis + ttlMillis;
        String result = Jwts.builder().setSubject(subject).setIssuer(issue)
                .setExpiration(new Date(expireMillis)).claim("user", claim).setId(issue)
                .signWith(getSignatureAlgorithm(), getSignedKey())
                .compressWith(CompressionCodecs.DEFLATE).compact();

        return result;
    }
    
    
      public static String getUsernameFromToken(String token)
   {
        Claims claims =getClaims(token);
        if(claims==null)
        {
          return "";
        }
        String username= claims.getSubject();
        return username;
    }
    
    private static Key getSignedKey()
    {
        byte[] apiKeySecretBytes = DatatypeConverter
                .parseBase64Binary(getAuthKey());
        Key signingKey = new SecretKeySpec(apiKeySecretBytes,
                getSignatureAlgorithm().getJcaName());
        return signingKey;
    }

    private static SignatureAlgorithm getSignatureAlgorithm()
    {
        return SignatureAlgorithm.HS256;
    }

    public static String getAuthKey()
    {
        String auth = JWT_SCRET_KEY;
        return auth;
    }

JWT认证过滤器

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter
{
    private Logger logger  = LoggerFactory.getLogger(getClass());
    
    @Autowired
    private JwtService jwtService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException
    {
        String uri=request.getRequestURI();
        
        //过滤无需认证请求
        
        
        // 从 HTTP 请求中获取 token
        String token = this.getTokenFromHttpRequest(request);
        logger.info("request token :{}",token);
        
        //传入token为空
        if(StringUtils.isEmpty(token))
        {
           logger.error("token is null");
           filterChain.doFilter(request,response);
           return; 
        }
        
        Authentication authentication = getAuthentication(token,request);
        
        //token验证失败
        if(StringUtils.isEmpty(authentication))
        {
           logger.error("authentication is null");
           filterChain.doFilter(request,response);
           return; 
        }
        
        // 将认证信息存入 上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
    }

    /**
     * 验证token
     * @param token
     * @return
     */
    private Authentication getAuthentication(String token,HttpServletRequest request) 
    {
        //从token中获取用户名
       String userName=JwtUtil.getUsernameFromToken(token);
       logger.info("getAuthentication userName:{}",userName);
       if(!StringUtils.isEmpty(userName) && SecurityContextHolder.getContext().getAuthentication() == null)
       {
           logger.info("getAuthentication username:{}"+userName);
           AuthedUser authedUser = jwtService.loadUserByUserId(userName);
           //验证token
           if(JwtUtil.validateToken(token))
           { 
               UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken= new UsernamePasswordAuthenticationToken(authedUser,null,authedUser.getAuthorities());
               usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
               return usernamePasswordAuthenticationToken;
           }
       }
       return null;
    }
    
    /**
     * 获取token信息
     * @param request
     * @return
     */
    private String getTokenFromHttpRequest(HttpServletRequest request) 
    {
        String token = request.getHeader("token");
        
        if(StringUtils.isEmpty(token))
        {
            token=request.getParameter("token");
        }
        return token;
    }

说明:前端传入token,经过过滤器对token进行验证,成功则将token保存到SecurityContextHolder上下文中。

JwtService业务类

@Service
@Transactional(rollbackFor=Exception.class)
public class JwtServiceImpl implements JwtService
{
    @Autowired
    private LoginService userService;

    public AuthedUser loadUserByUserId(String userId)
            throws UsernameNotFoundException
    {    
        AuthedUser authedUser=userService.loadUserByUsername(userId);
        if(authedUser==null)
        {
           throw new UsernameNotFoundException("用户名或密码错误");    
        }
        return authedUser;
    }
}

登录业务类

    public AuthedUser loadUserByUsername(String userId)
            throws UsernameNotFoundException
    {
        User user= userMapper.getUserByUserId(userId);
        
        if(user==null)
        {
            throw new RuntimeException("用户名或密码不正确");
        }
        
        //加载角色
        List<Role> roles= userMapper.getRolesByUserOid(user.getOid());
        if(CollectionUtils.isEmpty(roles))
        {
            throw new RuntimeException("用户角色为空");
        }
        
        Set<Integer> roleOids =new HashSet<>();
        for(Role role:roles)
        {
            roleOids.add(role.getOid());
        }
        
        //加载权限
        List<Func> funcs=userMapper.getResByRoleOid(roleOids);
        
        AuthedUser authedUser =new AuthedUser();
        authedUser.setUserName(userId);
        authedUser.setPassword(user.getPwd());
        //设置角色
        Set<Role> roleSet=new HashSet<>(roles);
        authedUser.setRoles(roleSet);
        //设置权限
        Set<Func> funcSet=new HashSet<>(funcs);
        authedUser.setResources(funcSet);
       return authedUser;
    }

loginService 接口继承UserDetailsService接口,重写loadUserByUsername方法。

认证实体类

public class AuthedUser implements UserDetails ,Serializable
{
    private static final long serialVersionUID = 600774151302261558L;
    
    private String userName;
    
    private String password;
    
    /**
     * 角色列表
     */
    private Set<Role> roles = new HashSet<Role>();
    
    /**
     * 权限列表
     */
    private Set<Func> resources = new HashSet<Func>();
    
 }   

修改Spring Security 配置

    /**
     * DSL编程
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login","/index","/login-error","/css/**","/js/**").permitAll()
                //任何请求需要认证
                .anyRequest().authenticated()
                // 表单认证、登录页面
                .and().formLogin().loginPage("/login")
                .failureUrl("/login-error").defaultSuccessUrl("/index").
                successHandler(customLoginSuccessHandler())
                //登出
                .and().logout().logoutUrl("/logout")
                //关闭跨域访问
                .and().csrf().disable()
                //未授权页面访问
                .exceptionHandling().accessDeniedPage( "/403" );
                //添加Token认证过滤器
                http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
                //未登录验证、鉴权异常
                http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .accessDeniedHandler(new CustomAccessDeineHandler());

                //使用无状态session,session不会储存用户状态
                http.sessionManagement()
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    }
    
    @Bean
    public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception
    {
        return new JwtAuthenticationFilter();
    }

说明:配置类中添加jwt认证过滤器。

测试

用户登录成功

图片.png

访问/app/sys/user/list接口,header中未携带token

图片.png

访问/app/sys/user/list接口,携带正常token

图片.png

总结

Spring Security 虽然比较重量级,但是提供了非常多的扩展性,可以实现多种方式认证,本文讲解了JWT认证实现,如有问题,可以随时反馈。