springboot+layui集成jwt改造知识要点

前言

最近有个项目用到jwt,jwt相比session的好处就是无状态stateless化,简单的讲,掉线或者网络波动不会导致重新登录,只要JWT有效即可继续请求。

#后端框架:SpringBoot+Freemarker+LayUI
#开源项目SpringBootCMS(https://github.com/moshowgame/SpringBootCMS)
#SpringBoot+SpringSecurity+JWT搭建手册
SpringBoot2+SpringSecurity整合JWT,前后端分离的API权限认证框架搭建手册(https://zhengkai.blog.csdn.net/article/details/96476554)

这里不做搭建的攻略,攻略请看以往的文章,其实搭建起来还是容易的,这里只提供一些改造的知识要点:

  1. 改造JwtRequestFilter,支持Authorization头和Token参数
  2. 改造JwtTokenUtil,在token头中存入更多参数
  3. 从login页面获取并设置jwt tokenlayui.data
  4. 设置模板,每个页面都进行jwt token处理
  5. (额外)后端从jwt中获取用户名等信息

参考文件:

方案

JwtRequestFilter

1.改造JwtRequestFilter,支持Authorization头和Token参数

package com.softdev.cms.config;

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

import com.softdev.cms.service.JwtUserDetailsService;
import com.softdev.cms.util.JwtTokenUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
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.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import io.jsonwebtoken.ExpiredJwtException;

/**
 * JwtRequestFilter
 * JWT请求过滤器:同时支持Authorization头+token参数处理模式
 * @author zhengkai.blog.csdn.net
 */
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUserDetailsService jwtUserDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.route.authentication.path}")
    private String authenticationPath;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String requestTokenHeader = request.getHeader("Authorization");
        String token = request.getParameter("token");
        String username = null;
        String jwtToken = null;
        // JWT报文表头的格式是"Bearer token". 去除"Bearer ",直接获取token
        // only the Token
        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException e) {
                System.out.println("Unable to get JWT Token");
            } catch (ExpiredJwtException e) {
                System.out.println("JWT Token has expired");
            }
        } else if(StringUtils.isNotEmpty(token)){
            System.out.println("token->"+token);
            jwtToken=token;
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException e) {
                System.out.println("Unable to get JWT Token");
            } catch (ExpiredJwtException e) {
                System.out.println("JWT Token has expired");
            }
            //param
        }else {
            //logger.warn("JWT Token does exists :"+request.getRequestURI());
        }
        // Once we get the token validate it.
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
            // if token is valid configure Spring Security to manually set
            // authentication
            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken
                        .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // After setting the Authentication in the context, we specify
                // that the current user is authenticated. So it passes the
                // Spring Security Configurations successfully.
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}

JwtTokenUtil

2.改造JwtTokenUtil,在token头中存入更多参数

package com.softdev.cms.util;

import com.softdev.cms.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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;

/**
 * JwtTokenUtil JWT工具类
 * @author zhengkai.blog.csdn.net
 */
@Component
public class JwtTokenUtil implements Serializable {
    private static final long serialVersionUID = -2550185165626007488L;
    public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;

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

    /**
     * 从JWT中解析Subject,一般是Username或者UserId
     */
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
    /**
     * 从JWT中解析RoleId
     */
    public Integer getRoleIdFromToken(String token) {
        return getAllClaimsFromToken(token).get("roleId",Integer.class);
    }
    /**
     * 从JWT中解析ShowName
     */
    public String getShowNameFromToken(String token) {
        return getAllClaimsFromToken(token).get("showName",String.class);
    }
    /**
     * 从JWT中解析过期时间
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }
    /**
     * 从JWT中解析所有Claims
     */
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }
    /**
     * 从JWT中判断是否过期
     */
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
    /**
     * 根据UserDetail(Security)生成Token
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        //设置额外信息
        return doGenerateToken(claims, userDetails.getUsername());
    }
    /**
     * 根据User(用户自定义)生成Token
     */
    public String generateToken(User user) {
        Map<String, Object> claims = new HashMap<>();
        //把对应的内容放到JWT报文中
        claims.put("showName",user.getShowName());
        claims.put("roleId",user.getRoleId());
        claims.put("phone",user.getPhone());
        claims.put("email",user.getEmail());
        //设置额外信息
        return doGenerateToken(claims, user.getUserName());
    }
    //while creating the token -
    //1. Define  claims of the token, like Issuer, Expiration, Subject, and the ID
    //2. Sign the JWT using the HS512 algorithm and secret key.
    //3. According to JWS Compact Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
    //   compaction of the JWT to a URL-safe string
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }
    /**
     * 校验JWT
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

login.html

  1. 从login页面获取并设置jwt token到layuidata

java登录成功返回jwt token信息

if(loginSuccess){
	//模式一:SpringSecurity默认生成token
    //final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    //String token = jwtTokenUtil.generateToken(userDetails);
    //模式二:自定义token生成规则(参照上面的jwtTokenUtil)
    String token = jwtTokenUtil.generateToken(user);
    return ReturnT.SUCCESS(token);
}else{
    return ReturnT.ERROR("登录失败,账号密码不正确");
}

前端接收jwt信息 layui.data('token', {key: "token",value: responseData.msg}); ,当然如果需要更多的信息,也可以封装更多的信息放回,或者返回整个userDTO然后前端解析和保存。

<div class="layui-container">
    <div class="admin-login-background">
        <div class="layui-form login-form">
            <form class="layui-form" action="">
                <div class="layui-form-item logo-title">
                    <h1>SpringBootCMS</h1>
                </div>
                <div class="layui-form-item">
                    <label class="layui-icon layui-icon-username" for="username"></label>
                    <input type="text" name="username" lay-verify="required|account" placeholder="账号" autocomplete="off" class="layui-input" value="admin">
                </div>
                <div class="layui-form-item">
                    <label class="layui-icon layui-icon-password" for="password"></label>
                    <input type="password" name="password" lay-verify="required|password" placeholder="密码" autocomplete="off" class="layui-input" value="123456">
                </div>
                <div class="layui-form-item">
                    <label class="layui-icon layui-icon-vercode" for="captcha"></label>
                    <input type="text" name="captcha" lay-verify="required|captcha" placeholder="图形验证码" autocomplete="off" class="layui-input verification captcha" value="">
                    <div class="captcha-img">
                        <img id="captchaPic" src="${request.contextPath}/captcha">
                    </div>
                </div>
                <div class="layui-form-item">
                    <input type="checkbox" name="rememberMe" value="true" lay-skin="primary" title="记住密码">
                </div>
                <div class="layui-form-item">
                    <button class="layui-btn layui-btn-fluid" lay-submit="" lay-filter="login">登 入</button>
                </div>
            </form>
        </div>
    </div>
</div>

<script>
    layui.use(['form', 'table','jquery'], function () {
        var $ = layui.jquery,
            form = layui.form,
            table = layui.table,
            layer = layui.layer;

        // 登录过期的时候,跳出ifram框架
        if (top.location != self.location) {
        	top.location = self.location;
        }

        // 粒子线条背景
        $(document).ready(function(){
            $('.layui-container').particleground({
                dotColor:'#5cbdaa',
                lineColor:'#5cbdaa'
            });
        });

        // 进行登录操作
        form.on('submit(login)', function (data) {
            data = data.field;
            if (data.username === '') {
                layer.msg('用户名不能为空');
                return false;
            }
            if (data.password === '') {
                layer.msg('密码不能为空');
                return false;
            }
            if (data.captcha === '') {
                layer.msg('验证码不能为空');
                return false;
            }
            $.ajax({
                type: 'POST',
                url: "${request.contextPath}/login",
                data:{
                    "username":data.username,
                    "password":data.password,
                    "captcha":data.captcha
                },
                //data:(JSON.stringify(jsonData)),
                //dataType: "json",
                //contentType: "application/json",
                success: function (responseData) {
                    if (responseData.code === 200) {
                        //使用layui存储返回的jwt token
                        layui.data('token', {
                            key: "token",
                            value: responseData.msg
                        });
                        //附加token参数跳转登陆成功首页
                        layer.msg("登录成功", function () {
                            window.location = '${request.contextPath}/index?token='+responseData.msg;
                        });
                    } else {
                        layer.msg(responseData.msg, function () {
                        });
                    }
                }
            });
            return false;
        });
    });
</script>

layui jwt token处理模板

  1. 设置模板,每个页面都进行jwt token处理

freemarker ftl模板,这里做了异常判断,非table页面直接设置会报错。

<#macro jwtHandle>
//author zhengkai.blog.csdn.net
$.ajaxSetup({
    headers: {
        "Author": "zhengkai.blog.csdn.net" ,
        "Authorization": "Bearer "+layui.data('token').token
    }
 });
 if(typeof(table)!="undefined"){
     table.set({
     	//其实这里可以不用两个都设置,选择头或者where即可
         headers: { //通过 request 头传递
             Authorization: "Bearer "+layui.data('token').token
         }
         ,where: { //通过参数传递
             token: layui.data('token').token
         }
     });
 }
</#macro>

无论是用freemarker或者什么框架,都不重要,只要能够每个页面引入进行设置即可。
如果实在不行,最差的方法就是每个页面就设置一次。

<#import "common/common-import.html" as netCommon>
<@netCommon.jwtHandle />

如果是编辑或者添加按钮,layer弹出层,则加一下 "&token="+layui.data('token').token 即可。

if (obj.event === 'edit') {
    var index = layer.open({
        title: '编辑',
        type: 2,
        shade: 0.2,
        maxmin:true,
        shadeClose: true,
        area: ['800px', '500px'],
        content: '${request.contextPath}/menu/edit?id='+obj.data.menuId+"&token="+layui.data('token').token,
    });
    return false;
} 

如果是使用layui的ajax或者table进行请求,在配置了jwt handle之后,就会自动设置请求头或者请求参数,相当于

$.ajax({
    type: 'POST',
     url: "${request.contextPath}/menu/list",
     data:{"searchParams":"{'parentMenuId':'0'}","page":"1","limit":"99"},
     headers: {
        "Author": "zhengkai.blog.csdn.net" ,
        "Authorization": "Bearer "+layui.data('token').token
    },
     success: function (responseData) {
         if (responseData.code === 200 || responseData.code === 0) {
             var length = responseData.data.length;
             console.log("parentMenuId.length:"+length);
             $("#parentMenuId").empty();
             $("#parentMenuId").append('<option value="">全部菜单</option>');
             $("#parentMenuId").append('<option value="0">主菜单</option>');
             for(var i = 0; i < length; i++) {
                 //添加option元素
                 $("#parentMenuId").append("<option value='" + responseData.data[i].menuId + "'>" + responseData.data[i].title + "</option>");
             }
             $("#parentMenuId").val("");
             form.render('select');
         } else {
             layer.msg("加载主菜单列表失败:"+responseData.msg, function () {
                 //window.location = '/index.html';
             });
         }
     }
 });
table.render({
            elem: '#currentTableId',
            method: 'post',
            url: '${request.contextPath}/menu/list',
            where: { //通过参数传递
             	token: layui.data('token').token
         	},
            toolbar: '#toolbarDemo',
            defaultToolbar: ['filter', 'exports', 'print', {
                title: '提示',
                layEvent: 'LAYTABLE_TIPS',
                icon: 'layui-icon-tips'
            }],
            cols: [[
                {field: 'menuId', title: 'ID', sort: true},
                {field: 'title',  title: '名称', sort: true},
                {field: 'href', title: '链接', sort: true},
                {field: 'icon', title: '图标', sort: true},
                {field: 'parentMenuId',  title: '父菜单ID', sort: true},
                {title: '操作', minWidth: 50, templet: '#currentTableBar', fixed: "right", align: "center"}
            ]],
            limits: [20, 50 , 100],
            limit: 20,
            page: true
        });

JwtTokenUtil从token参数获取信息

  1. (额外)后端从jwt中获取用户名等信息
@GetMapping("/display")
public ModelAndView display(Integer activityId,String token){
	//@Author zhengkai.blog.csdn.net
	//设置token参数,接收前端传回的token
	//从token中解析返回userName,当然也可以有其他有用信息,根据实际情况进行设置。
    String userName = jwtTokenUtil.getUsernameFromToken(token);
    //利用username查询用户
    User user= userMapper.selectOne(new QueryWrapper<User>().eq("user_name",userName));
    //查询活动
    Activity activity = activityMapper.selectOne(new QueryWrapper<Activity>().eq("activity_id",activityId));
    //返回到活动签到页面,携带用户和活动信息
    return new ModelAndView("cms/activitySign-display","activity",activity).addObject("activityId",activityId).addObject("loginUser",user);
}

猜你喜欢

转载自blog.csdn.net/moshowgame/article/details/106533520