Spring Security实现JWT帐号密码验证

2019-08-26  本文已影响0人  一块自由的砖

什么是JWT

JWT介绍和使用

实现原理

在security框架,增加自定义的jwt filter,通过继承OncePerRequestFilter,实现对每次请求的请求头进行处理。从请求头中获取 JWT Bearer tonken,对tonken进行解析和判定。

实现步骤

1 配置Maven
2 JWT相关工具类
3 定义Domain和Dto的数据结构
4 实现自定义过滤器,主要负责从请求头中读取token,解析token
5 实现Login controller
6 配置Security,定义那些请求需要进行jwt验证

具体实现

1 配置Maven,增加jwt包依赖

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

2 JWT相关工具类,JWT编解码

package com.springboot.action.saas.modules.security.utils;

import com.springboot.action.saas.modules.security.dto.UserDetailsDto;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtUtil implements Serializable {

    private static final long serialVersionUID = -3301605591108950415L;
    //jwt包提供的时间对象
    private Clock clock = DefaultClock.INSTANCE; //时间工具实例
    //Header,Payload两部分的签名
    @Value("${jwt.secret}")
    private String secret;
    //超期时间
    @Value("${jwt.expiration}")
    private Long expiration;
    //http请求头字段
    @Value("${jwt.header}")
    private String tokenHeader;

    /*
     * 解析jwt字符串
     * @param token
     * @return Claims对象,jwt信息提供给的一个类
     * */
    private Claims getAllClaimsFromToken(String token) {
        //解析jwt到claims对象
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    /*
     * 判定是否token超期
     * @param token
     * @return boolean
     * */
    private Boolean isTokenExpired(String token) {
        //获取token超期时间
        final Date expiration = getExpirationDateFromToken(token);
        //判定expiration对象表示的瞬间比clock.now()表示的瞬间早,返回为true
        return expiration.before(clock.now());
    }
    /*
     * 获取token外带数据字段,这里是用户名称
     * @param token
     * @return 用户名称
     * */
    public String getUsernameFromToken(String token) {
        //获取jwt对象
        final Claims claims = getAllClaimsFromToken(token);
        return claims.getSubject();
    }

    /*
     * 获取token签发时间字段
     * @param token
     * @return 用户名称
     * */
    public Date getIssuedAtDateFromToken(String token) {
        //获取jwt对象
        final Claims claims = getAllClaimsFromToken(token);
        return claims.getIssuedAt();
    }

    /*
     * 获取token超期时间字段
     * @param token
     * @return 超期时间Date对象
     * */
    public Date getExpirationDateFromToken(String token) {
        //获取jwt对象
        final Claims claims = getAllClaimsFromToken(token);
        return claims.getExpiration();
    }

    /*
     * 判定是否签发时间,是否在修改密码之前
     * @param created 签发时间
     * @param lastPasswordReset 最后一次密码修改时间
     * @return boolean
     * */
    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    /*
     * 生成超期日期对象
     * @param createdDate 发放token日期对象
     * @return Date 超期日期对象
     * */
    private Date calculateExpirationDate(Date createdDate) {
        //时间戳初始化Date对象
        return new Date(createdDate.getTime() + expiration);
    }
    /*
     * 判定是否token超期
     * @param userDetails
     * @return jwt字符串
     * */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        final Date createdDate = clock.now();
        final Date expirationDate = calculateExpirationDate(createdDate);
        //创建jwt
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /*
     * 刷新token
     * @param token token字符串
     * @return jwt字符串
     * */
    public String refreshToken(String token) {
        final Date createdDate = clock.now();
        final Date expirationDate = calculateExpirationDate(createdDate);

        final Claims claims = getAllClaimsFromToken(token);
        claims.setIssuedAt(createdDate);
        claims.setExpiration(expirationDate);

        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
    /*
     * 判定token是否合法
     * @param token token字符串
     * @param userDetails 验证用户数据
     * @return Date 超期日期对象
     * */
    public Boolean validateToken(String token, UserDetails userDetails) {
        //获取认证的用户信息
        UserDetailsDto user = (UserDetailsDto)userDetails;
        //获取token中的用户名称
        final String username = getUsernameFromToken(token);
        //获取token中的签发时间
        final Date created = getIssuedAtDateFromToken(token);
        //获取用户重置密码时间
        Date lastPasswordReset = new Date(user.getPasswordResetDate());
        //判定token:用户是否合法,是否超期,判定是否
        return (
                username.equals(user.getUsername())
                        && !isTokenExpired(token)
                        && !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
        );
    }
}

3 定义Domain和Dto的数据结构
3.1 用户登陆发送用户信息数据(LoginPwdDto.java)

package com.springboot.action.saas.modules.security.dto;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;


//用户帐号密码登陆,用户和密码
@Data
//lombok 注解,NoArgsConstructor 走动生成无参数构造函数
@NoArgsConstructor
public class LoginPwdDto implements Serializable {
    //用户帐号
    private String username;
    //用户密码
    private String password;
    //对象字符串输出
    @Override
    public String toString() {
        return "{username="
                + username
                + ", password=******}";
    }
}

3.2 用户登陆授权通过后给用户返信息数据(LoginInfoDto.java)

package com.springboot.action.saas.modules.security.dto;

import lombok.Data;

import java.io.Serializable;

//用户认证成功后,返回用户信息和jwt的token
@Data
public class LoginInfoDto implements Serializable {

    private String token;

    private UserDetailsDto user;

    public LoginInfoDto(String token, UserDetailsDto user) {
        this.token = token;
        this.user = user;
    }
}

3.3 fliter和认证service之间传递信息数据 (UserDetailsDto.java)

package com.springboot.action.saas.modules.security.dto;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

//这个类,一定比userDto要数据更全面,因为UserDetails在认证后UserDetailsServiceImpl的
//loadUserByUsername函数获取的信息
@Getter
//生成一个全参数的构造函数
@AllArgsConstructor
public class UserDetailsDto implements UserDetails {
    //用户idjson返回时不显示)
    //在json序列化时将java bean中的一些属性忽略掉
    @JsonIgnore
    private final Long id;
    //用户名称(接口规范必须实现)
    private final String username;
    //用户密码(json返回时不显示,接口规范必须实现)
    @JsonIgnore
    private final String password;
    //是否禁用
    private final Boolean enabled;

    @Override
    public boolean isEnabled() {
        return enabled;
    }
    //创建时间
    private final Long createTime;
    //密码重置时间(json返回时不显示)
    @JsonIgnore
    private final Long passwordResetDate;
    //用户权限列表(接口规范必须实现)
    @JsonIgnore
    private final Collection<GrantedAuthority> authorities;
    //(接口规范必须实现)
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    //(接口规范必须实现)
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    //(接口规范必须实现)
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
}

4 实现自定义过滤器,主要负责从请求头中读取token,解析token

package com.springboot.action.saas.modules.security.filter;


import com.springboot.action.saas.modules.security.dto.UserDetailsDto;
import com.springboot.action.saas.modules.security.utils.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/*
* 自定义过滤器,主要负责从请求头中读取token,解析token
* 检查token是否合法
* */
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    //认证用户信息业务对象
    private final UserDetailsService userDetailsService;
    //jwt 工具对象
    private final JwtUtil jwtUtil;
    //获取http请求头的key
    private final String tokenHeader;

    /*
    * 构造函数
    * @param @Qualifier用来标记service有两个实现类的时候,用那个
    * @param jwtUtil jwt工具对象
    * @param tokenHeader http请求头token字段
    * */
    public JwtAuthorizationFilter(@Qualifier("userDetailsServiceImpl") UserDetailsService userDetailsService,
                                  JwtUtil jwtUtil,
                                  @Value("${jwt.header}") String tokenHeader) {
        this.userDetailsService = userDetailsService;
        this.jwtUtil = jwtUtil;
        this.tokenHeader = tokenHeader;
    }
    /*
    * 抽象类oncePerRequestFilter继承自GenericFilterBean,它保留了GenericFilterBean中的所有方法并对之进行了扩展,
    * 在oncePerRequestFilter中的主要方法是doFilter。在doFilter方法中,doFilterInternal方法由子类实现,
    * 主要作用是规定过滤的具体方法。
    * */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        //获取请求头中key对应的字段
        final String requestHeader = request.getHeader(this.tokenHeader);

        String username = null;
        String token = null;
        //处理请求头中的token
        if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
            //获取token
            token = requestHeader.substring(7);
            //获取用户名(外带数据)
            try {
                username = jwtUtil.getUsernameFromToken(token);
            } catch (ExpiredJwtException e) {
                //发生异常,需要记录日志
                //log.error(e.getMessage());
            }
        }
        //用户名判定
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            //获取用户信息
            UserDetailsDto userDetailsDto = (UserDetailsDto)this.userDetailsService.loadUserByUsername(username);
            //判定token是否合法,不合法走异常处理exceptionHandling().authenticationEntryPoint
            if (jwtUtil.validateToken(token, userDetailsDto)) {
                //合法,创建带用户名和密码以及权限的Authentication,这里实例化UsernamePasswordAuthenticationToken
                //构造函数内实际上已经设置为认证通过super.setAuthenticated(true);
                //构造函数3个参数:
                // 用户信息(身份认证信息,还有其他外带信息都可以增加)
                // 用户密码(于证明principal是正确的信息,比如密码)
                // 用户权限(授权信息,比如角色)
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetailsDto,
                        null,
                        userDetailsDto.getAuthorities());
                //设置获取request的一些http信息,把http的信息放到authentication
                authentication.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                );
                //记录日志
                //log.info("authorizated user '{}', setting security context", username);
                //从SecurityContextHolder获取SecurityContext实例,设置authentication
                //已验证的主体,或删除身份验证信息
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        //调用后续filter
        chain.doFilter(request, response);
    }
}

5 实现Login controller,用户验证帐号密码通过后,返回jwt的信息

package com.springboot.action.saas.modules.security.controller;

import com.springboot.action.saas.common.logging.annotation.Log;
import com.springboot.action.saas.common.utils.EncryptionUtils;
import com.springboot.action.saas.modules.security.dto.LoginInfoDto;
import com.springboot.action.saas.modules.security.dto.LoginPwdDto;
import com.springboot.action.saas.modules.security.dto.UserDetailsDto;
import com.springboot.action.saas.modules.security.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.persistence.EntityNotFoundException;
import java.util.List;

/*
 *  restful 风格接口
 * */
//@RestController 代替 @Controller,省略以后的 @ResponseBody
@RestController
//处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。
@RequestMapping("/security")
public class SecurityController {
    //jwt工具对象,根据类型来查找和自动装配元素的
    @Autowired
    private JwtUtil jwtUtil;
    //认证用户信息对象
    @Autowired
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    /**
     * 用户帐号,密码登陆
     * @param loginPwdDto 用户的帐号和密码
     * @return LoginInfoDto
     */
    @Log("帐号登陆")
    @PostMapping(value = "/pwdlogin")
    public LoginInfoDto pwdlgoin(@Validated @RequestBody LoginPwdDto loginPwdDto) {
        try {
            //获取用户认证信息
            final UserDetailsDto userDetailsDto = (UserDetailsDto)userDetailsService.loadUserByUsername(loginPwdDto.getUsername());
            //判定密码是否正确
            final String userPassword = EncryptionUtils.encryptPassword(loginPwdDto.getPassword());
            if (!userDetailsDto.getPassword().equals(userPassword)) {
                //密码错误处理,抛异常
                throw new AccountExpiredException("密码错误");
            }
            //判定用户是否启动
            if (!userDetailsDto.isEnabled()) {
                //处理帐号禁用,抛异常
                throw new AccountExpiredException("帐号被禁用");
            }
            //生成token
            String token = jwtUtil.generateToken(userDetailsDto);
            //返回认证信息对象
            return new LoginInfoDto(token, userDetailsDto);
        } catch (EntityNotFoundException e) {
                //检查用户名是否存在
                throw new AccountExpiredException("用户不存在");
        }
    }
}

6 配置Security,用jwt的认证接管默认的帐号密码认证

package com.springboot.action.saas.modules.security.config;

import com.springboot.action.saas.modules.security.filter.JwtAuthorizationFilter;
import com.springboot.action.saas.modules.security.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

//定义配置类被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被
//AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,
//并用于构建bean定义,初始化Spring容器。
@Configuration
//加载了WebSecurityConfiguration配置类, 配置安全认证策略。
//加载了AuthenticationConfiguration,
@EnableWebSecurity
//用来构建一个全局的AuthenticationManagerBuilder的标志注解
//开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认
@EnableGlobalMethodSecurity(prePostEnabled = true)
//Web Security 配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //实现UserDetailService接口用来做登录认证
    @Autowired
    private UserDetailsServiceImpl userDetailsServiceImpl;
    //自定义基于JWT的安全过滤器,bean
    @Autowired
    JwtAuthorizationFilter authenticationFilter;
    /*
     * 配置http服务,路径拦截、csrf保护等等均可通过此方法配置
     * */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //HttpSecurity对象
        httpSecurity
                // 禁用 CSRF,不然post调试的时候都403
                .csrf().disable()
                // 由于使用jwt,不创建会话
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 设置权限定义哪些URL需要被保护、哪些不需要被保护。HttpSecurity对象的方法
                .authorizeRequests()
                // 过滤请求,允许对网站静态资源的访问,无需授权
                .antMatchers(
                    HttpMethod.GET,
                    "/*.html",
                    "/favicon.ico",
                    "/**/*.html",
                    "/**/*.css",
                    "/**/*.js"
                ).permitAll()
                // 登陆页面,无需授权
                .antMatchers(HttpMethod.POST, "/security/pwdlogin").permitAll()
                // 调试期间先允许访问
                //.antMatchers("/member/**").permitAll()
                // 认证通过后任何请求都可访问。AbstractRequestMatcherRegistry的方法
                .anyRequest().authenticated()
                // 连接HttpSecurity其他配置方法
                .and()
                // 生成默认登录页,HttpSecurity对象的方法
                .formLogin();
        // 增加jwt filter
        httpSecurity
                .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);

    }
    /**
     * 设定PsswordEncoder为BeanBcrypt加密方式,后面在设定AuthenticationProvider需要用到
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoderBean() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 创建认证提供者Bean
     * DaoAuthenticationProvider是SpringSecurity提供的AuthenticationProvider默认实现类
     * 授权方式提供者,判断授权有效性,用户有效性,在判断用户是否有效性,
     * 它依赖于UserDetailsService实例,可以自定义UserDetailsService的实现。
     *
     * @return
     */
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        // 创建DaoAuthenticationProvider实例
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        // 将自定义的认证逻辑添加到DaoAuthenticationProvider
        authProvider.setUserDetailsService(userDetailsServiceImpl);
        // 设置自定义的密码加密
        authProvider.setPasswordEncoder(passwordEncoderBean());
        return authProvider;
    }

    /*
     * 配置好的认证提供者列表
     *
     * */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 添加自定义的认证逻辑
        auth.authenticationProvider(authenticationProvider());
    }
}
上一篇 下一篇

猜你喜欢

热点阅读