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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章