美文网首页
Spring Security实现JWT帐号密码验证

Spring Security实现JWT帐号密码验证

作者: 一块自由的砖 | 来源:发表于2019-08-26 15:05 被阅读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());
        }
    }
    

    相关文章

      网友评论

          本文标题:Spring Security实现JWT帐号密码验证

          本文链接:https://www.haomeiwen.com/subject/srerectx.html