springcloud實現各個模塊的統一登陸邏輯和關鍵代碼

標題起的有點問題,因爲統一登陸不一定實在同一個項目的不同模塊,也可以是不同項目的不同模塊。既可以是一個大的springcloud項目,也可以是多個分屬於不同項目的springboot項目。

1.首先要理解何爲統一登陸?

統一登陸可以通過一個具體的app來實現。下面是一張美團app的首頁截圖。

首頁上面的美食,電影演出,酒店住宿,休閒娛樂,外賣五大模塊都整合到這個app裏。但是要知道美團之前就是美團外賣,後來的這些功能都是整合進來的。分屬於不同的獨立引用或者app中,肯定有自己獨立的登陸。這些應用的不在一個模塊甚至項目中。但是整合到美團app後,只要在app這層登陸後,這五大模塊的登陸態就變成已登錄狀態,從而訪問裏面的信息不會應爲再次登陸而被登陸攔截。一次登陸後,在這五個功能模塊中都是已經登陸,便是統一登陸!

統一登陸絕不是美團app登陸後,在電腦網頁版就也是登錄態,這是不可能實現的能力。統一登陸是一種整合能力的表現。

在這裏插入圖片描述

2.實現統一登陸的技術選擇

美團外賣的統一登陸或許有別的實現統一登陸的方式。上面只是以app的首頁講解何爲統一登陸。這裏的實現是我們公司的具體實現,和美團無關。

實現對請求的攔截處理有兩種方式: implements javax.servlet.Filterimplements org.springframework.web.servlet.HandlerInterceptor

HandlerInterceptor在方法的preHandle,postHandle,afterCompletion實現攔截邏輯。Filter是在方法 doFilter()中實現攔截邏輯。

但是這裏要有個清楚的認識Spring的Interceptor與Servlet的Filter區別點

Filter作用於servlet容器,Interceptor作用於spring容器。一個請求過來經過二者的先後順序是這樣的。

Filter前處理 --> Interceptor前處理 --> action --> Interceptor後處理 --> Filter後處理
  1. 如果只是單純的登錄校驗,將校驗的代碼放在Filter裏面會更好,因爲這樣請求都不會到Spring裏面就已經被Servlet拒絕了,這樣可以更加好的保證Spring的安全。
  2. 如果項目需要根據權限去攔截,需要根據用戶信息去查詢數據庫表。這就不可避免的要使用Spring的組件了,這種情況下就使用Interceptor處理好一些。
  3. 但是對於springboot項目,通過對filter註解@Component,即 org.springframework.stereotype.Component來讓spring來管理。也是可以在filter中實現注具體service層的。

3.項目中的技術實現思路

對於springcloud項目的統一登陸大都放在zuul網關層實現,對登陸進行校驗,攔截。對於zuul下的項目都需要登陸校驗的項目來說是沒有問題。但是我們的項目的登陸需求並不是都需要。有寫還不需要登陸。大致是這樣的。項目一,二和訂單模塊需要登陸,但是三方支付模塊和open模塊是不需要登陸的。而這些都在zuul網關的治理下。如果在zuul網關加上登陸校驗,則不需要登陸的模塊就會請求攔截,顯示未登錄進不了服務模塊。
所以我們採用在各自模塊上通過filter實現各自的登陸校驗。而登陸要滿足各個項目實現統一登陸。
在這裏插入圖片描述

技術實現簡單說就是:jwt實現token回傳給客戶端,客戶端的每次請求都帶着token給後臺。token+filter實現登陸態校驗。token+session實現一次解析用戶信息,項目層面更簡單獲取用戶信息。

4.技術的具體實現

用戶註冊登陸接口(filter放行)返回生成的token,而後所有請求會被filter攔截進行token校驗。

  • 4.1 引入jwt的pom依賴
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
  • 4.2 通過jwt生成token的工具類
public class Constant {
	public static final String TOKEN               = "token";
	public static final String EXPIRE_TIME               = "expireTime";
	public static final String USER_ID = "userId";
	public static final String LOGIN_NAME = "loginName";
	public static final String USER_NAME = "userName";
	public static final String MOBILE = "mobile";
	public static final String IP_ADDRESS = "ipAddress";
}



import com.server.constant.Constant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

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

@SuppressWarnings("Duplicates")
@Component
public class TokenComponent {

    private             Logger log                =LoggerFactory.getLogger(TokenComponent.class);
    public static final String CLAIM_KEY_USER     = "userId";
    public static final String CLAIM_KEY_CREATED  = "created";
    public static final String CLAIM_KEY_MOBILE   = "mobile";
    public static final String CLAIM_KEY_USERNAME = "userName";
    public static final String CLAIM_KEY_LOGINNAME = "loginName";

    private String secret = "secret";

    /**
     * 7 days
     */
    private Long defaultExpiration = 604800L;

    /**
     * 生成token
     * @param mobile 手機
     * @param userId 用戶ID
     * @param userName 用戶名稱
     * @param expiration 過期時間
     * @return TOKEN
     */
    public Map<String, String> generateToken(String userId, String mobile, String userName,String loginName, Long expiration) {
        Map<String, Object> claims = new HashMap<>(5);
        claims.put(CLAIM_KEY_USER, userId);
        claims.put(CLAIM_KEY_MOBILE, mobile);
        claims.put(CLAIM_KEY_CREATED, new Date());
        claims.put(CLAIM_KEY_USERNAME, userName);
        claims.put(CLAIM_KEY_LOGINNAME, loginName);
        Date expirationDate;
        if (expiration == null) {
            expirationDate = new Date(System.currentTimeMillis() + defaultExpiration * 1000);
        } else {
            expirationDate = new Date(System.currentTimeMillis() + expiration * 1000);
        }

        String token = Jwts.builder()
                .setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
        Map<String, String> result = new HashMap<>();
        result.put(Constant.TOKEN,token);
        result.put(Constant.EXPIRE_TIME,expirationDate.getTime()+"");
        return result;
    }

    /**
     * 解析token
     * @param token
     * @return 用戶信息的Map集合:Claims 
     */
    public Claims getClaimsFromToken(String token) {
        log.info("getShopId request header Authorization={}", token);
        try {

            return Jwts
                    .parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            log.error("商家未登錄:{}", e);
            throw new JwtException(e.getMessage());
        }
    }

    /**
     * 判斷token是不失效
     * @param token
     * @return 是否失效
     */
    public Boolean isTokenExpire(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expire = claims.getExpiration();
            return !expire.before(new Date());
        } catch (Exception e) {
            log.error("商家未登錄:{}", e);
            throw new JwtException(e.getMessage());
        }
    }
}



import com.service.token.TokenComponent;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;


public class SessionUtil {

	public static Map<String,String> saveSession(RedisService redisService, HttpServletRequest request, String userId, String
			loginName, String
			userName, String mobile, TokenComponent tokenComponent){
		Map<String,String> tokenAndExpireTime = tokenComponent.generateToken(userId,mobile,userName,loginName,30*60L);
		HttpSession session = request.getSession();
		String token =  tokenAndExpireTime.get(Constant.TOKEN);
		session.setAttribute(Constant.TOKEN, token);
		session.setAttribute(Constant.USER_ID, userId);
		session.setAttribute(Constant.LOGIN_NAME, loginName);
		session.setAttribute(Constant.USER_NAME, userName);
		session.setAttribute(Constant.MOBILE, mobile);
		session.setAttribute(Constant.IP_ADDRESS, RequestUtil.getIpAddress(request));
		// 刷新session有效時間30分鐘
		session.setMaxInactiveInterval(604800);
		return tokenAndExpireTime;
	}
}
  • 4.3 登錄註冊接口中生成token,同時將用戶信息保存session中。

SessionUtil.saveSession在上面這個代碼塊中。生成token等返回給前端。

			Map<String, String> tokenAndExpireTime = SessionUtil.saveSession(redisService, request, loginVo.getId(),
					loginVo.getLoginName(),
					loginVo.getUserName(), mobile, tokenComponent);
			result.put(ID, loginVo.getId());
			result.put(TOKEN, tokenAndExpireTime.get(Constant.TOKEN));
			result.put(EXPIRE_TIME,tokenAndExpireTime.get(Constant.EXPIRE_TIME));
			LOGGER.info("loginVo.getId():" + loginVo.getId());
			return ResponseJson.success(RESPONSE_CODE_SUCCESS, result);
  • 4.4 前端請求登錄以後的接口

在filter爲放開,則要進行登錄校驗。校驗通過則將用戶信息寫入session。

@Component
@RefreshScope
@Data
public class YmlConfig {
    /**
     * @Description filter
     */
    @Value("${filter}")
    private String filter;
}


import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.util.StringUtil;
import com.ly.mt.mall.config.YmlConfig;
import com.ly.mt.mall.service.redis.service.RedisService;
import com.ly.mt.mall.service.token.TokenComponent;
import com.ly.mt.mall.util.Constant;
import com.ly.mt.mall.util.FilterUtil;
import com.ly.mt.mall.util.RequestUtil;
import com.ly.mt.mall.vo.ResponseJson;
import io.jsonwebtoken.Claims;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import static com.ly.mt.mall.service.redis.RedisKey.REDIS_TOKEN_LOGIN_MALL_H5;
import static com.ly.mt.mall.vo.ResponseCode.RESPONSE_CODE_NOT_LOGIN;

@Component
public class MyFilter implements Filter {
    private final static Logger LOGGER = LoggerFactory.getLogger(MyFilter.class);
    @Autowired
    private YmlConfig yml;
    @Autowired
    private TokenComponent tokenComponent;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest servletRequest,
                         ServletResponse servletResponse,
                         FilterChain filterChain) {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        try {
            HttpSession session = request.getSession();
            LOGGER.info("1請求sessionId={},url={}", session.getId(), request.getRequestURL());
            String uri = request.getRequestURI();
            // 放行地址
            if (FilterUtil.checkFilter(uri, yml.getFilter())) {
                filterChain.doFilter(servletRequest, servletResponse);
                return;
            }
            String jwtToken = request.getHeader(Constant.TOKEN);
            if (StringUtil.isNotEmpty(jwtToken)) {
                Claims claims = tokenComponent.getClaimsFromToken(jwtToken);
                LOGGER.info("===================================userCache:" + claims.toString());
                if (StringUtil.isEmpty(claims.toString())) {
                    LOGGER.info("用戶名或token爲空,未登錄攔截");
                    return;
                }
                session.setAttribute(Constant.USER_ID, claims.get(TokenComponent.CLAIM_KEY_USER));
                session.setAttribute(Constant.USER_NAME, claims.get(TokenComponent.CLAIM_KEY_USERNAME));
                session.setAttribute(Constant.LOGIN_NAME, claims.get(TokenComponent.CLAIM_KEY_LOGINNAME));
                session.setAttribute(Constant.MOBILE, claims.get(TokenComponent.CLAIM_KEY_MOBILE));
                session.setAttribute(Constant.TOKEN, jwtToken);
                session.setAttribute(Constant.IP_ADDRESS, RequestUtil.getIpAddress(request));
            }
            // 未登錄攔截
            String loginName = String.valueOf(session.getAttribute(Constant.LOGIN_NAME));
            String token = String.valueOf(session.getAttribute(Constant.TOKEN));
            if (StringUtil.isEmpty(loginName) || StringUtil.isEmpty(token)) {
                LOGGER.info("用戶名或token爲空,未登錄攔截");
                notLogin(loginName, session, request, response);
                return;
            }
            filterChain.doFilter(servletRequest, servletResponse);
        } catch (Exception e) {
            LOGGER.error("filter執行異常:", e);
            return;
        }
    }

    /**
     * @Description 未登錄攔截
     */
    private void notLogin(String loginName, HttpSession session, HttpServletRequest request, HttpServletResponse response) {
        LOGGER.info("!!!!!!!!!!未登錄攔截,銷燬session,清空cookie");
        // 銷燬session
        session.invalidate();
        // 刪除cookie token
        Cookie tokenCookie = getTokenCookie(request);
        if (null != tokenCookie) {
            tokenCookie.setMaxAge(0);
            tokenCookie.setPath("/");
            response.addCookie(tokenCookie);
        }
    }


    /**
     * @Description 獲取返回前端的token cookie
     */
    private Cookie getTokenCookie(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (null == cookies) {
            return null;
        }
        for (Cookie cookie : cookies) {
            if (Constant.TOKEN.equals(cookie.getName())) {
                return cookie;
            }
        }
        return null;
    }
}

4.6 如果登錄成功。session中時保存一份用戶信息的。通過封裝用戶信息到基本service類中,然後新定義的類繼承這個類,即可隨時取用用戶基本信息。

import com.util.Constant;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * 模塊公共接口,處理本模塊公共方法
 *
 * @author zhanglifeng
 */
@Service
public class BaseServiceImpl  implements BaseService {

    @Override
    public String getLoginUserId() {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        Assert.notNull(sra, "sra must not be null");
        HttpServletRequest request = sra.getRequest();
        HttpSession session = request.getSession();
        return String.valueOf(session.getAttribute("userId"));
    }

    @Override
    public String getLoginUserName() {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        Assert.notNull(sra, "sra must not be null");
        HttpServletRequest request = sra.getRequest();
        HttpSession session = request.getSession();
        return String.valueOf(session.getAttribute("userName"));
    }

    @Override
    public String getLoginUserIp() {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        Assert.notNull(sra, "sra must not be null");
        HttpServletRequest request = sra.getRequest();
        HttpSession session = request.getSession();
        return String.valueOf(session.getAttribute("ipAddress"));
    }

    @Override
    public String getLoginUserMobile() {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        Assert.notNull(sra, "sra must not be null");
        HttpServletRequest request = sra.getRequest();
        HttpSession session = request.getSession();
        return String.valueOf(session.getAttribute(Constant.MOBILE));
    }
}

在這裏插入圖片描述

以上便是其中一個項目的登錄配置。其中filter中放開攔截的部分根據模塊對應的業務,會靈活配置相應的放開接口地址。都是通過前臺獲取的token中拿到用戶登錄信息。並存放到session中

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章