5. SpringBoot+SpringSecurity+Jwt权限认证---认证

1. 整体逻辑

1. SpringSecurity认证的逻辑规则

启动项目时,SpringBoot自动检索所有带@Configuration的注解,所以就将我们的WebSecurityConfig给加载了,这个config中,我们需要在configure(AuthenticationManagerBuilder auth)方法中注册一个继承自UserDetailsService的接口,这个接口中只有一个方法,那就是使用username获取到数据库中用户信息并返回成UserDetail实体。这个方法需要我们按照我们的不同业务场景重写

WebSecurityConfig


/**
 * @description:
 * @author: coderymy
 * @create: 2020-10-01 13:54
 * <p>
 * 1\. 创建WebSecurityConfig 类继承WebSecurityConfigurerAdapter
 * 2\. 类上加上@EnableWebSecurity,注解中包括@Configuration注解
 * <p>
 * WebSecurityConfigurerAdapter声明了一些默认的安全特性
 * (1)验证所有的请求
 * (2)可以使用springSecurity默认的表单页面进行验证登录
 * (3)允许用户使用http请求进行验证
 */

/**
 * 如何自定义认证
 * 1\. 实现并重写configure(HttpSecurity http)方法,鉴权,也就是判断该用户是否有访问该api的权限
 * <p>
 * <p>
 * 页面显示403错误,表示该用户授权失败(401代表该用户认证失败)前端可以使用返回的状态码来标识如何给用户展示
 * 用2XX表示本次操作成功,用4XX表示是客户端导致的失败,用5XX表示是服务器引起的错误
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    public static void main(String[] args) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode("123");
        System.out.println(encode);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }

    /**
     * SpringSecurity5.X要求必须指定密码加密方式,否则会在请求认证的时候报错
     * 同样的,如果指定了加密方式,就必须您的密码在数据库中存储的是加密后的,才能比对成功
     *
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //鉴权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 1\. HttpSecurity被声明为链式调用
         * 其中配置方法包括
         *  1\. authorizeRequests()url拦截配置
         *  2\. formLogin()表单验证
         *  3\. httpBasic()表单验证
         *  4\. csrf()提供的跨站请求伪造防护功能
         */
        /**
         * 2\. authorizeRequests目的是指定url进行拦截的,也就是默认这个url是“/”也就是所有的
         * anyanyRequest()、antMatchers()和regexMatchers()三种方法来拼配系统的url。并指定安全策略
         */
        http.authorizeRequests()
                //这里指定什么样的接口地址的请求,需要什么样的权限 ANT模式的URL匹配器
                .antMatchers("/select/**").hasRole("USER")//用户可以有查询权限
                .antMatchers("/insert/**").hasRole("ADMIN")//管理员可以有插入权限权限
                .antMatchers("/empower/**").hasRole("SUPERADMIN")//超级管理员才有赋权的权限
                .antMatchers("/login/**").permitAll()//标识list所有权限都可以直接访问,即使不登录也可以访问。一般将login页面放给这个权限
                .and()
                .formLogin()
//                .loginProcessingUrl("/login/user")//用来定义什么样的API请求时login请求
//                .permitAll()//login请求需要是所有权限都可以的
                .and().csrf().disable();

        /**
         * 将自定义的JWT过滤器加入configure中
         */
        JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());
        http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);
    }

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    //认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }

}

MyUserDetailsService


@Service
public class MyUserDetailsService implements UserDetailsService {

    @Resource
    private UsersRepository usersRepository;

    /**
     * 其实这样就完成了认证的过程,能获取到数据库中配置的用户信息
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //获取该用户的信息
        Users user = usersRepository.findByUsername(username);

        if (user == null) {//用户不存在报错
            throw new UsernameNotFoundException("用户不存在");
        }

        /**
         * 将roles信息转换成SpringSecurity内部的形式,即Authorities
         * commaSeparatedStringToAuthorityList可以将使用,隔开的角色列表切割出来并赋值List
         * 如果不行的话,也可以自己实现这个方法,只要拆分出来就可以了
         */
        //注意,这里放入Authorities中的信息,都需要是以Role_开头的,所以我们在数据库中配置的都是这种格式的。当我们使用hasRole做比对的时候,必须要是带Role_开头的。否则可以使用hasAuthority方法做比对
        user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));

        return user;
    }
}

其实如果去掉上面的将自定义的JWT过滤器加入到过滤链中的话,这个认证过程已经完成了。使用下面的代码就可以调用起整个认证程序。

核心代码

authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));

这一行就会将username和password放到认证程序中进行认证。

也就是需要我们自己也逻辑让他去触发这个代码的实现。就可以自动完成认证程序了。就会触发使用username获取到数据库用户信息,然后经过密码加密比对之后会将认证结果返回。

我们整合JWT其实也很简单,其实就是将JWT的登录部分的操作,使用过滤器封装,将该过滤器放到整个认证的过滤链中

2. 自定义JWT过滤器的配置

SpringSecurity过滤器的配置无非以下几个条件

  1. 该过滤器过滤什么样的API请求(也就是说什么样的API请求会触发该过滤器执行)。配置被过滤的请求API
  2. 该过滤器做什么事
  3. 该过滤器执行成功以及执行失败的各种情况该怎么做
  4. 该过滤器执行的时机是什么样的,也就是在过滤链之前还是之后执行

先解决逻辑上以上三个问题的答案

  1. 我们需要拦截认证请求,肯定是形如xxx/login/xxx这种API接口的请求啦
  2. 这个过滤器会做什么事呢?
    1. 首先,我们需要进行用户名密码的基础验证,也就是合不合法
    2. 我们需要调用起SpringSecurity的默认认证程序
    3. 认证程序执行成功之后,我们需要按照用户的信息以JWT的规则生成一个JWTToken并将其放入response中返回回去
    4. 认证程序执行失败,也就是用户登录失败,我们也需要将返回信息封装起来返回给用户
  3. 执行成功,我们需要返回给用户JWTToken信息。执行失败,我们也要友好提示用户
  4. 如果排除其他的业务场景干扰,目前过滤链只有进行鉴权时候才使用。所以针对不同的业务场景,这个过滤器放的地方其实是不一样的。(之后我们的另一个JWTToken校验的过滤器应该需要在这个认证的过滤器之后(两个其实并不捕捉同样的APi所以不会依次执行。也不用太考虑这个问题))(我记得SpringCloud中的zuul网关的过滤器是可以自定义级别的。但是目前在SpringSecurity中尚未发现这种功能)

针对以上解答,下面用代码来做展示(ps:序号依次对应上面)

  1. 配置过滤器过滤地址
    /**
     * 下面是为了配置这个Manager
     * 配置其拦截的API请求地址
     *
     * @param authenticationManager
     */
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl("/login/user");//这里指定什么样的API请求会被这个过滤器拦截

    }

  1. 配置过滤器职能
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //这里是定义一个拦截器,在认证方面的拦截器,当请求的时候回拦截到这里面然后进行身份认证
        //验证用户名密码是否正确之后
        Authentication authenticate = null;
        try {
            System.out.println("InputStream:" + request.getInputStream());
            UserDto userDto = new ObjectMapper().readValue(request.getInputStream(), UserDto.class);//这个地方相当于封装一下请求,因为前台请求的是user.username="xxx"这种对象的形式。
            //对于这个过滤器拦截的接口,去调用SpringSecurity默认的认证程序,也就是去进行SpringSecurity的认证
            authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));
            return authenticate;
            //如果返回成功,就进入successfulAuthentication。返回失败就进入unsuccessfulAuthentication
            //可以通过下面的定义来让前端得到不同的返回从而向用户展示不同的效果
            //TODO 这个地方还有点问题,如果密码错误了,就会报BadCredentialsException错误。需要看一下如果不让这么报错并让他进入到unsuccessfulAuthentication方法中
        } catch (BadCredentialsException e) {//捕捉密码验证错误异常
            log.info("密码错误");
            try {
                this.unsuccessfulAuthentication(request, response, e);
            } catch (IOException ex) {
                ex.printStackTrace();
            } catch (ServletException ex) {
                ex.printStackTrace();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        //当身份验证通过之后,会进入这里,这里可以定义成功的返回
        Users user = (Users) authResult.getPrincipal();//将principal中的信息转换成User对象

        JwtUtil util = new JwtUtil(secretKey, SignatureAlgorithm.HS256);

        Map<String, Object> map = new HashMap<>();
        map.put("username", user.getUsername());
        map.put("password", user.getPassword());

        String jwtToken = util.encode("tom", 30000, map);

        response.addHeader("Authorizations", jwtToken);
        user.setJwtToken(jwtToken);
        ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.success(user)));
        System.out.println(response.getHeaderNames());
//        super.successfulAuthentication(request, response, chain, authResult);
        //注意,不要使用默认的super来定义,否则上述会失效的
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        //TODO 得看一下为啥会报这个错误
        System.out.println("认证失败");

        ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.error("登录失败,账号密码错误")));
    }

  1. 执行失败与成功,分别是2中的unsuccessfulAuthenticationsuccessfulAuthentication方法
  2. 配置过滤链执行的位置
在WebSecurityConfig中的configure(HttpSecurity http)方法中

JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());
http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);

完成了以上的配置,前台就可以使用/login/user来进行登录操作了。登录成功会返回一个JSON对象来供前端判断成功与否

2. 代码结果

全部代码奉上,随意写的注释有点多,不看的可以给删掉

  1. WebSecurityConfig
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @description:
 * @author: coderymy
 * @create: 2020-10-01 13:54
 * <p>
 * 1\. 创建WebSecurityConfig 类继承WebSecurityConfigurerAdapter
 * 2\. 类上加上@EnableWebSecurity,注解中包括@Configuration注解
 * <p>
 * WebSecurityConfigurerAdapter声明了一些默认的安全特性
 * (1)验证所有的请求
 * (2)可以使用springSecurity默认的表单页面进行验证登录
 * (3)允许用户使用http请求进行验证
 */

/**
 * 如何自定义认证
 * 1\. 实现并重写configure(HttpSecurity http)方法,鉴权,也就是判断该用户是否有访问该api的权限
 * <p>
 * <p>
 * 页面显示403错误,表示该用户授权失败(401代表该用户认证失败)前端可以使用返回的状态码来标识如何给用户展示
 * 用2XX表示本次操作成功,用4XX表示是客户端导致的失败,用5XX表示是服务器引起的错误
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    public static void main(String[] args) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode("123");
        System.out.println(encode);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }

    /**
     * SpringSecurity5.X要求必须指定密码加密方式,否则会在请求认证的时候报错
     * 同样的,如果指定了加密方式,就必须您的密码在数据库中存储的是加密后的,才能比对成功
     *
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //鉴权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 1\. HttpSecurity被声明为链式调用
         * 其中配置方法包括
         *  1\. authorizeRequests()url拦截配置
         *  2\. formLogin()表单验证
         *  3\. httpBasic()表单验证
         *  4\. csrf()提供的跨站请求伪造防护功能
         */
        /**
         * 2\. authorizeRequests目的是指定url进行拦截的,也就是默认这个url是“/”也就是所有的
         * anyanyRequest()、antMatchers()和regexMatchers()三种方法来拼配系统的url。并指定安全策略
         */
        http.authorizeRequests()
                //这里指定什么样的接口地址的请求,需要什么样的权限 ANT模式的URL匹配器
                .antMatchers("/select/**").hasRole("USER")//用户可以有查询权限
                .antMatchers("/insert/**").hasRole("ADMIN")//管理员可以有插入权限权限
                .antMatchers("/empower/**").hasRole("SUPERADMIN")//超级管理员才有赋权的权限
                .antMatchers("/login/**").permitAll()//标识list所有权限都可以直接访问,即使不登录也可以访问。一般将login页面放给这个权限
                .and()
                .formLogin()
//                .loginProcessingUrl("/login/user")//用来定义什么样的API请求时login请求
//                .permitAll()//login请求需要是所有权限都可以的
                .and().csrf().disable();

        /**
         * 将自定义过滤器加入configure中
         */
        JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());
        http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);
    }

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    //认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }

}

  1. JWTAuthenticationFilter
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huanong.avatarma.basic.entity.Users;
import com.huanong.avatarma.basic.model.vo.UserDto;
import com.huanong.avatarma.common.util.JwtUtil;
import com.huanong.avatarma.common.util.ResponseUtil;
import com.huanong.avatarma.common.util.ResultUtil;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

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

/**
 * @description: jwt拦截器
 * 这个定义完成之后,想要生效还需要将其加入到过滤链中
 * @author: coderymy
 * @create: 2020-10-02 09:36
 **/
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    /**
     * 验证用户名密码是否正确之后,生成一个token,并将token返回客户端
     *
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */

    private String secretKey = "ILoveDanChaoFan";

    private AuthenticationManager authenticationManager;

    /**
     * 下面是为了配置这个Manager
     * 配置其拦截的API请求地址
     *
     * @param authenticationManager
     */
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl("/login/user");//这里指定什么样的API请求会被这个过滤器拦截

    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //这里是定义一个拦截器,在认证方面的拦截器,当请求的时候回拦截到这里面然后进行身份认证
        //验证用户名密码是否正确之后
        Authentication authenticate = null;
        try {
            System.out.println("InputStream:" + request.getInputStream());
            UserDto userDto = new ObjectMapper().readValue(request.getInputStream(), UserDto.class);//这个地方相当于封装一下请求,因为前台请求的是user.username="xxx"这种对象的形式。
            //对于这个过滤器拦截的接口,去调用SpringSecurity默认的认证程序,也就是去进行SpringSecurity的认证
            authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));
            return authenticate;
            //如果返回成功,就进入successfulAuthentication。返回失败就进入unsuccessfulAuthentication
            //可以通过下面的定义来让前端得到不同的返回从而向用户展示不同的效果
            //TODO 这个地方还有点问题,如果密码错误了,就会报BadCredentialsException错误。需要看一下如果不让这么报错并让他进入到unsuccessfulAuthentication方法中
        } catch (BadCredentialsException e) {//捕捉密码验证错误异常
            log.info("密码错误");
            try {
                this.unsuccessfulAuthentication(request, response, e);
            } catch (IOException ex) {
                ex.printStackTrace();
            } catch (ServletException ex) {
                ex.printStackTrace();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        //当身份验证通过之后,会进入这里,这里可以定义成功的返回
        Users user = (Users) authResult.getPrincipal();//将principal中的信息转换成User对象

        JwtUtil util = new JwtUtil(secretKey, SignatureAlgorithm.HS256);

        Map<String, Object> map = new HashMap<>();
        map.put("username", user.getUsername());
        map.put("password", user.getPassword());

        String jwtToken = util.encode("tom", 30000, map);

        response.addHeader("Authorizations", jwtToken);
        user.setJwtToken(jwtToken);
        ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.success(user)));
        System.out.println(response.getHeaderNames());
//        super.successfulAuthentication(request, response, chain, authResult);
        //注意,不要使用默认的super来定义,否则上述会失效的
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        //TODO 得看一下为啥会报这个错误
        System.out.println("认证失败");

        ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.error("登录失败,账号密码错误")));
    }

}

  1. MyUserDetailsService
import com.huanong.avatarma.basic.dao.UsersRepository;
import com.huanong.avatarma.basic.entity.Users;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
 * @description:
 * @author: coderymy
 * @create: 2020-10-01 15:55
 **/
@Service
public class MyUserDetailsService implements UserDetailsService {
    @Resource
    private UsersRepository usersRepository;
    /**
     * 其实这样就完成了认证的过程,能获取到数据库中配置的用户信息
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //获取该用户的信息
        Users user = usersRepository.findByUsername(username);
        if (user == null) {//用户不存在报错
            throw new UsernameNotFoundException("用户不存在");
        }
        /**
         * 将roles信息转换成SpringSecurity内部的形式,即Authorities
         * commaSeparatedStringToAuthorityList可以将使用,隔开的角色列表切割出来并赋值List
         * 如果不行的话,也可以自己实现这个方法,只要拆分出来就可以了
         */
        //注意,这里放入Authorities中的信息,都需要是以Role_开头的,所以我们在数据库中配置的都是这种格式的。当我们使用hasRole做比对的时候,必须要是带Role_开头的。否则可以使用hasAuthority方法做比对
        user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
        return user;
    }
}

  1. Users
@Data
@ToString
@Entity
@Table(name = "users")
public class Users implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    private boolean enable;

    private String roles;

    private Date createDate;

    private Date modifyDate;

    @Transient//这个注解可以帮助在entity中添加表中没有的字段
    private List<GrantedAuthority> authorities;

    @Transient
    private String jwtToken;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //这个本身是对应roles字段的,但是因为结构不一致,所以重新创建一个,后续补充这部分
        return this.authorities;
    }

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

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

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

    public void setAuthorities(List<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public boolean isEnabled() {
        return this.enable;
    }
}

  1. Result
import lombok.Data;
import lombok.ToString;

/**
 * @description:
 * @author: coderymy
 * @create: 2020-10-02 13:24
 **/
@Data
@ToString
public class Result {

    private Integer code;

    private String msg;

    private Object data;

}

  1. JwtUtil
/**
 * @description:
 * @author: coderymy
 * @create: 2020-10-02 09:24
 **/
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;

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

import java.util.*;

/*
 * 总的来说,工具类中有三个方法
 * 获取JwtToken,获取JwtToken中封装的信息,判断JwtToken是否存在
 * 1\. encode(),参数是=签发人,存在时间,一些其他的信息=。返回值是JwtToken对应的字符串
 * 2\. decode(),参数是=JwtToken=。返回值是荷载部分的键值对
 * 3\. isVerify(),参数是=JwtToken=。返回值是这个JwtToken是否存在
 * */
public class JwtUtil {
    //创建默认的秘钥和算法,供无参的构造方法使用
    private static final String defaultbase64EncodedSecretKey = "badbabe";
    private static final SignatureAlgorithm defaultsignatureAlgorithm = SignatureAlgorithm.HS256;

    public JwtUtil() {
        this(defaultbase64EncodedSecretKey, defaultsignatureAlgorithm);
    }

    private final String base64EncodedSecretKey;
    private final SignatureAlgorithm signatureAlgorithm;

    public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
        this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
        this.signatureAlgorithm = signatureAlgorithm;
    }

    /*
     *这里就是产生jwt字符串的地方
     * jwt字符串包括三个部分
     *  1\. header
     *      -当前字符串的类型,一般都是“JWT”
     *      -哪种算法加密,“HS256”或者其他的加密算法
     *      所以一般都是固定的,没有什么变化
     *  2\. payload
     *      一般有四个最常见的标准字段(下面有)
     *      iat:签发时间,也就是这个jwt什么时候生成的
     *      jti:JWT的唯一标识
     *      iss:签发人,一般都是username或者userId
     *      exp:过期时间
     *
     * */
    public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
        //iss签发人,ttlMillis生存时间,claims是指还想要在jwt中存储的一些非隐私信息
        if (claims == null) {
            claims = new HashMap<>();
        }
        long nowMillis = System.currentTimeMillis();

        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)
                .setId(UUID.randomUUID().toString())//2\. 这个是JWT的唯一标识,一般设置成唯一的,这个方法可以生成唯一标识
                .setIssuedAt(new Date(nowMillis))//1\. 这个地方就是以毫秒为单位,换算当前系统时间生成的iat
                .setSubject(iss)//3\. 签发人,也就是JWT是给谁的(逻辑上一般都是username或者userId)
                .signWith(signatureAlgorithm, base64EncodedSecretKey);//这个地方是生成jwt使用的算法和秘钥
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);//4\. 过期时间,这个也是使用毫秒生成的,使用当前时间+前面传入的持续时间生成
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    //相当于encode的方向,传入jwtToken生成对应的username和password等字段。Claim就是一个map
    //也就是拿到荷载部分所有的键值对
    public Claims decode(String jwtToken) {

        // 得到 DefaultJwtParser
        return Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(base64EncodedSecretKey)
                // 设置需要解析的 jwt
                .parseClaimsJws(jwtToken)
                .getBody();
    }

    //判断jwtToken是否合法
    public boolean isVerify(String jwtToken) {
        //这个是官方的校验规则,这里只写了一个”校验算法“,可以自己加
        Algorithm algorithm = null;
        switch (signatureAlgorithm) {
            case HS256:
                algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
                break;
            default:
                throw new RuntimeException("不支持该算法");
        }
        JWTVerifier verifier = JWT.require(algorithm).build();
        verifier.verify(jwtToken);  // 校验不通过会抛出异常
        //判断合法的标准:1\. 头部和荷载部分没有篡改过。2\. 没有过期
        return true;
    }

    public static void main(String[] args) {
        JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
        //以tom作为秘钥,以HS256加密
        Map<String, Object> map = new HashMap<>();
        map.put("username", "tom");
        map.put("password", "123456");
        map.put("age", 20);

        String jwtToken = util.encode("tom", 30000, map);

        System.out.println(jwtToken);
        util.decode(jwtToken).entrySet().forEach((entry) -> {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        });
    }
}

  1. ResponseUtil、ResultUtil、repository等不做展示。需要的可以联系我,这个是基础性的东西
  2. 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.11.0</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.10.RELEASE</version>
</dependency>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349