SpringSecurity权限管理系统实战—6、SpringSecurity整合JWT

目录

SpringSecurity权限管理系统实战—1、项目简介和开发环境准备
SpringSecurity权限管理系统实战—2、日志、接口文档等实现
SpringSecurity权限管理系统实战—3、主要页面及接口实现
SpringSecurity权限管理系统实战—4、整合SpringSecurity(上)
SpringSecurity权限管理系统实战—5、整合SpringSecurity(下)
SpringSecurity权限管理系统实战—6、SpringSecurity整合jwt
SpringSecurity权限管理系统实战—7、处理一些问题
SpringSecurity权限管理系统实战—8、AOP 记录用户日志、异常日志css

前言

最近是真的懒,感受我每月都有那么几天什么都不想干。。html

画风一转,前几天的lpl忍界大战是真的精彩,虚假的电竞春晚:RNG vs IG 。真正的电竞春晚 TES vs IG。TES自从阿水和kasra加入以后,状态直接起飞,在我看来TES将是s10夺冠热门之一。不过这一次木叶村打败了晓组织。前端

本觉得会打满三局,没想到ig直接2:0带走。rookie线上压制了新皇knight,确实永远能够相信宋义进,或许是由于‍小钰采访吧。java

这两把我最没想到的是kasra被宁王压着打,几乎没有节奏,宝蓝在哪都是阿水的噩梦。这波啊,这波是盗版打赢了正版,puff小小的证实了本身。git

最后仍是但愿lpl的饭圈粉少一点,peacegithub

在这里插入图片描述

进入正题web

1、无状态登陆

  • 有状态登陆算法

    咱们知道在原始的项目中咱们是经过session和cookie来实现用户的识别认证。可是这样作无疑会增长服务器的压力,服务的保存了大量的数据。若是业务须要扩展,搭建了集群的话,还须要将session共享。spring

  • 无状态登陆json

    而什么是无状态登陆呢,简而言之,就是服务的不须要再保存任何的用户信息,而是用户本身携带者信息去访问服务端,服务端经过这些信息来识别客户端身份。这样一来,有状态登陆的缺点都被解决了,可是这一样也会带来新问题。好比token信息没法在服务端注销,必需要等其本身过时,占用更多的空间(意味着须要更多带宽),修改密码后本来的token在没过时时仍然可用访问系统等。

2、JWT介绍

一、什么是jwt

JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种能够安全传输的 小巧 和 自包含 的JSON对象。因为数据是使用数字签名的,因此是可信任的和安全的。JWT可使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。

咱们来看一下jwt长什么样

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJjb2Rlcm15IiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU5NjA4MDM5OX0.rfDtzMus50uAFnqMw1tm3c_ZYbmUNkIRqMkeJ0510PAH-RCUWtZkfNPTDYAGVVfDU6jmdEkGyNYvGy3UrNq5pA

JSON Web 令牌以紧凑的形式由三个部分组成,由点分隔,它们包括:

  • 头部
  • 负载
  • 签名

头部(Header)

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 一般直接使用 HMAC SHA256

像这样

{
  'typ': 'JWT',
  'alg': 'HS256'
}

载荷(Payload)

这个部分用来承载要传递的数据,他的默认字段有

  • iss:发行人
  • exp:到期时间
  • sub:主题
  • aud:用户
  • nbf:在此以前不可用
  • iat:发布时间
  • jti:JWT ID用于标识该JWT

除以上默认字段外,咱们还能够自定义私有字段,例如

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

签名(Signature)

Signature 部分是对前两部分的签名,防止数据篡改。

二、JWT工做流程

  • 用户发起登陆请求
  • 服务端验证身份,将用户信息,标识等信息打包成jwt token返回给客户端
  • 用户拿到token,携带token发送请求给服务端
  • 服务的验证token是否可用,可用便根据其y业务逻辑返回相应结果。

三、简单实现

首先咱们在maven中引入如下依赖

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

新建JwtTest来测试一下

/**
 * @author codermy
 * @createTime 2020/7/30
 */
public class JwtTest {
    public static void main(String[] args) {
        String token = Jwts.builder()
                //用户名
                .setSubject("codermy")
                //自定义属性 放入用户拥有请求权限
                .claim("authorities","admin")
                // 设置失效时间为1分钟
                .setExpiration(new Date(System.currentTimeMillis()+1000*60))
                // 签名算法和密钥
                .signWith(SignatureAlgorithm.HS512, "java")
                .compact();
        System.out.println(token);
    }

输出

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJjb2Rlcm15IiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU5NjA4MDM5OX0.rfDtzMus50uAFnqMw1tm3c_ZYbmUNkIRqMkeJ0510PAH-RCUWtZkfNPTDYAGVVfDU6jmdEkGyNYvGy3UrNq5pA

咱们再来解析

//解析token
        Claims claims = Jwts.parser()
                .setSigningKey("java")
                .parseClaimsJws(token)
                .getBody();
        System.out.println(claims);
        //获取用户名
        String username = claims.getSubject();
        System.out.println("username:"+username);
        //获取权限
        String authority = claims.get("authorities").toString();
        System.out.println("权限:"+authority);
        System.out.println("到期时间:" + claims.getExpiration());

输出

{sub=codermy, authorities=admin, exp=1596082316}
username:codermy
权限:admin
到期时间:Thu Jul 30 12:11:56 CST 2020

3、整合JWT

后端实现

其实jwt自己很好理解,无非就就是一把钥匙,可用打开对应的锁,这不过这把钥匙稍微特殊点,它还带了主人的一些信息。难理解的是要将它符合业务逻辑的整合进框架中。我本身就被绕了很久才明白。

我这里写了一个Jwt的工具类,用于生成和解析jwt

/**
 * @author codermy
 * @createTime 2020/7/23
 */
@Component
public class JwtUtils {
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private  Long expiration;
    // 建立token
    public  String generateToken(String username) {
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, secret)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .compact();

    }
    // 从token中获取用户名
    public  String getUserNameFromToken(String token){
        return getTokenBody(token).getSubject();
    }

    // 是否已过时
    public  boolean isExpiration(String token){
        return getTokenBody(token).getExpiration().before(new Date());
    }

    private  Claims getTokenBody(String token){
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

}

而后咱们能够将jwt的一些信息写在yml中,使得能够灵活的配置。application.yml中添加以下配置

jwt:
  tokenHeader: Authorization #JWT存储的请求头
  secret: my-springsecurity-plus #JWT加解密使用的密钥
  expiration: 604800 #JWT的超期限时间(60*60*24*7)
  tokenHead: 'Bearer ' #JWT负载中拿到开头,空格别忘了

咱们照着jwt的工做流程来,首先是登陆成功后客户端会返回一个jwt token

因此咱们首先自定义一个MyAuthenticationSuccessHandler继承AuthenticationSuccessHandler,这是登陆成功后的处理器

/**
 * @author codermy
 * @createTime 2020/8/1
 * 登陆成功
 */
@Component
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    private JwtUtils jwtUtils;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {

        JwtUserDto userDetails = (JwtUserDto)authentication.getPrincipal();//拿到登陆用户信息
        String jwtToken = jwtUtils.generateToken(userDetails.getUsername());//生成token
        Result result = Result.ok().message("登陆成功").jwt(jwtToken);
        System.out.println(JSON.toJSONString(result));//用于测试
        httpServletResponse.setCharacterEncoding("utf-8");//修改编码格式
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));//输出结果
        httpServletResponse.sendRedirect("/api/admin");//重定向到api/admin页面。我这里路由名取的不是很好
    }
}

而后咱们再写一个jwt的拦截器,让每一个请求都须要验证jwt token

/**
 * @author codermy
 * @createTime 2020/7/30
 */
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private JwtUtils jwtUtils;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String authHeader = request.getHeader(this.tokenHeader);//拿到requset中的head
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
            String username = jwtUtils.getUserNameFromToken(authToken);//解析token获取用户名
            log.info("checking username:{}", username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                if (userDetails != null) {//判断是否存在这个给用户
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    log.info("authenticated user:{}", username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
    }

这里为了以后结果更直观,自定义一个AuthenticationEntryPoint,用于在未登陆是访问接口返回json而不是login.html

/**
 * @author codermy
 * @createTime 2020/8/1
 * 当未登陆或者token失效访问接口时,自定义的返回结果
 */
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");//设置编码格式
        response.setContentType("application/json");
        response.getWriter().println(JSON.toJSONString(Result.error().message("还没有登陆,或者登陆过时   " + authException.getMessage())));
        response.getWriter().flush();
    }
}

将上述方法加入到SpringSecurityConfig中

/**
 * @author codermy
 * @createTime 2020/7/15
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private VerifyCodeFilter verifyCodeFilter;
    @Autowired
    MyAuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    @Autowired
    private RestfulAccessDeniedHandler accessDeniedHandler;
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }


    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers(HttpMethod.GET,
                        "/swagger-resources/**",
                        "/PearAdmin/**",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/swagger-ui.html",
                        "/webjars/**",
                        "/v2/**");//放行静态资源
    }

    /**
     * anyRequest          |   匹配全部请求路径
     * access              |   SpringEl表达式结果为true时能够访问
     * anonymous           |   匿名能够访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户彻底认证能够访问(非remember-me下自动登陆)
     * hasAnyAuthority     |   若是有参数,参数表示权限,则其中任何一个权限能够访问
     * hasAnyRole          |   若是有参数,参数表示角色,则其中任何一个角色能够访问
     * hasAuthority        |   若是有参数,参数表示权限,则其权限能够访问
     * hasIpAddress        |   若是有参数,参数表示IP地址,若是用户IP和参数匹配,则能够访问
     * hasRole             |   若是有参数,参数表示角色,则其角色能够访问
     * permitAll           |   用户能够任意访问
     * rememberMe          |   容许经过remember-me登陆的用户访问
     * authenticated       |   用户登陆后可访问
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
        http.csrf().disable()//关闭csrf
                .sessionManagement()// 基于token,因此不须要session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .httpBasic().authenticationEntryPoint(restAuthenticationEntryPoint)//未登录时返回 JSON 格式的数据给前端,不然是html
                .and()
                .authorizeRequests()
                .antMatchers("/captcha").permitAll()//任何人都能访问这个请求
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登陆页面 不设限访问
                .loginProcessingUrl("/login")//拦截的请求
                .successHandler(authenticationSuccessHandler) // 登陆成功处理器
                .permitAll()
                // 防止iframe 形成跨域
                .and()
                .headers()
                .frameOptions()
                .disable()
                .and();

        // 禁用缓存
        http.headers().cacheControl();

        // 添加JWT拦截器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


}

我这里直接贴了完整的代码,由于有添加也有删除,不是很好描述,你们对比着以前的来看,都添加了注释。

如今咱们重启项目,用admin帐号来登陆。登陆成功后发现页面并无跳转到咱们想去的页面,可是控制台打印出了咱们想要的jwt信息

{"code":200,"data":[],"jwt":"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTU5NjI1OTgyOCwiZXhwIjoxNTk2ODY0NjI4fQ.Khn5t6WjOsuG6R2if1Q_gAeNq-zTamIAO32b1UVc6L8-6_IAHMaCeWr-v7H2-7Hob0SSmmK23dv71_da-YK8hw","msg":"登陆成功","success":true}

这是为何呢?

着很好理解,由于咱们的jwt拦截器已经起了做用,而咱们本来的前端页面是没有把jwt token添加在header上的,因此认为没有登陆,重定向到了登陆页面。

可是咱们如今能够借助postman来测试,postman是一个测试api的工具,你们能够自行百度,这里不作过多介绍。

在咱们未携带jwt token信息时,访问http://localhost:8080/api/menu接口,就会报以下错误

在这里插入图片描述

咱们在header中添加上,以前登陆成功控制台打印的token信息(由于咱们添加了图片验证码,因此登陆不是很方便用postman,咱们能够在浏览器中登陆或者先把验证码的拦截器去除)

在这里插入图片描述

加上了token信息以后再去访问http://localhost:8080/api/menu接口,发现已经能够正常访问了

在这里插入图片描述

咱们再尝试用test用户登陆后获取到jwt token访问该接口,会报以下错误
在这里插入图片描述

修改Swagger配置

直接贴代码

/**
 * @author codermy
 * @createTime 2020/7/10
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;


    @Bean
    public Docket createRestApi() {
        ParameterBuilder ticketPar = new ParameterBuilder();
        List<Parameter> pars = new ArrayList<>();
        ticketPar.name(tokenHeader).description("token")
                .modelRef(new ModelRef("string"))
                .parameterType("header")
                .defaultValue(tokenHead + " ")
                .required(true)
                .build();
        pars.add(ticketPar.build());
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(webApiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.codermy.myspringsecurityplus.controller"))
                .paths(PathSelectors.any())
                .paths(Predicates.not(PathSelectors.regex("/error.*")))

                .build()
                .globalOperationParameters(pars);
    }
    /**
     * 该套 API 说明,包含做者、简介、版本、等信息
     * @return
     */
    private ApiInfo webApiInfo(){
        return new ApiInfoBuilder()
                .title("my-springsecurity-plus-API文档")
                .description("本文档描述了my-springsecurity-plus接口定义")
                .version("1.0.5")
                .build();
    }

}

如今再swagger中就能够添加token测试了
在这里插入图片描述

前端适配

那么咱们如今已经简单的实现了jwt的无状态登陆功能,须要作的就是让前端的请求都带上jwt token。

。。。研究了半天没弄懂,因此暂时先搁置,下一章解决它。有知道怎么设置请求头的小伙伴也能够留言告诉我

因此本章结束的代码是不能正常在浏览器运行的,可是能够在postman和swagger中测试(若是想运行,在SpringSecurityConfig中添加上.rememberMe()便可)

giteegithub中可获取源代码,与本系列文章同步更新

相关文章
相关标签/搜索