springboot +shiro +jwt 整合,禁用seesion

Shiro的Session機制

因爲個人方法是改變了Shiro的默認的Session機制,因此這裏先簡單講一下Shiro的機制,簡單瞭解Shiro是怎麼肯定每次訪問的是哪一個用戶的css

Servlet的Session機制

Shiro在JavaWeb中使用到的就是默認的Servlet的Session機制,大體流程以下:html

 

 

1.用戶首次發請求前端

2.服務器接收到請求以後,不管你有沒有權限訪問到資源,在返回響應的時候,服務器都會生成一個Session用來儲存該用戶的信息,而後生成SessionId做爲對應的Keyjava

3.服務器會在響應中,用jsessionId這個名字,把這個SessionId以Cookie的方式發給客戶(就是Set-Cookie響應頭)web

4.因爲已經設置了Cookie,下次訪問的時候,服務器會自動識別到這個SessionId而後找到你上次對應的Sessionspring

Shiro帶來的變化

而結合Shiro以後,上面的第二步和第三步會發生小變化:數據庫

2.—>服務器不但會建立Session,還會建立一個Subject對象(就是Shiro中用來表明當前用戶的類),也用這個SessionId做爲Key綁定apache

3.—>第二次接受到請求的時候,Shiro會從請求頭中找到SessionId,而後去尋找對應的Subject而後綁定到當前上下文,這時候Shiro就能知道來訪的是誰了json

 

 

個人思路

主要思想是:用JWT Token來代替Shiro本來返回的Sessionapi

 

工做流程:

  • 用戶登陸
  • 若成功則JWT生成token,返回給前端
  • 用戶第二次攜帶JWT來訪問接口
  • 服務器解析JWT,認證成功
  • 服務器會校驗是否有權限

 

 

代碼實現

1、導入JWT相關包

  1.一、pom.xml裏寫入

<dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.1</version>
        </dependency>

 

1.二、  JWT工具類

JwtUtils,代碼以下:

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.tg.higo.couponplatform.entity.dto.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * JWT憑證驗證工具
 *
 * @author LuoJianXing
 */
public class JwtUtils {
    
    private static final Algorithm ALGORITHM;
    
    /**
     * 登陸Token有效期
     */
    public static final Integer VALID_SECONDS = 3600 * 10;
    
    private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
    
    static {
        ALGORITHM = Algorithm.HMAC256("tghigo");
    }
    
    /**
     * 編碼登陸用戶
     *
     * @param user
     * @param validSecond
     * @return
     */
    public static String encodeToken(User user, int validSecond) {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, validSecond);

        JSONObject obj = new JSONObject();
        obj.put("account",user.getAccount());
        obj.put("role",user.getRole());
        if (StringUtil.isNotEmpty(user.getOrgId())){
            obj.put("orgId", user.getOrgId());
        }
        String token = JWT.create().withClaim("user", obj.toJSONString())
                .withExpiresAt(calendar.getTime())
                .sign(ALGORITHM);

        return token;
    }


    
    public static String encodeToken(User user) {
        return encodeToken(user, VALID_SECONDS);
    }
    
    /**
     * 解碼登陸用戶
     *
     * @param token
     * @return
     */
    public static User decodeToken(String token) {
        DecodedJWT decodedJWT = JWT.decode(token);
        String userStr = decodedJWT.getClaim("user").as(String.class);
        return JSONObject.parseObject(userStr, User.class);
    }
    
    
    /**
     * 驗證JWT Token
     *
     * @param token
     * @return
     */
    public static boolean validToken(String token) {
        if (StringUtil.isBlank(token)) {
            return false;
        }
        
        return JWT.decode(token).getExpiresAt().after(Calendar.getInstance().getTime());
    }


    //編碼api方式登陸用戶
    public static String encode(String openid, int validSecond) {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, validSecond);
        return JWT.create()
                .withClaim("openid", openid)
                .withExpiresAt(calendar.getTime()).sign(ALGORITHM);
    }



    //解碼api方式登陸用戶
    public static String decode(String token) {

        DecodedJWT decodedJWT = JWT.decode(token);
        return decodedJWT.getClaim("openid").as(String.class);
    }
    




}

 

 

2、整合shiro

2.一、導入shiro相關包 pom.xml裏寫入

<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.5.1</version>
        </dependency>

 

2.二、 關閉 自動建立session

package com.tg.higo.couponplatform.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;

public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {

    @Override
    public Subject createSubject(SubjectContext context) {
        // 不建立 session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}

 

 

2.三、自定義過濾器

package com.tg.higo.couponplatform.shiro;

import com.tg.higo.couponplatform.common.utils.StringUtil;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtFilter extends AccessControlFilter {

    /**
     * 日誌對象
     */
    protected Logger logger = LoggerFactory.getLogger(AdminShiroRealm.class);


    /*
     * 1. 返回true,shiro就直接容許訪問url
     * 2. 返回false,shiro纔會根據onAccessDenied的方法的返回值決定是否容許訪問url
     * */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        logger.warn("isAccessAllowed 方法被調用");
        //這裏先讓它始終返回false來使用onAccessDenied()方法
        return false;
    }

    /**
     * 返回結果爲true代表登陸經過
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        logger.warn("onAccessDenied 方法被調用");
        //這個地方和前端約定,要求前端將jwtToken放在請求的Header部分

        //因此之後發起請求的時候就須要在Header中放一個Authorization,值就是對應的Token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = getTokenFromRequest(request);
        if(StringUtil.isBlank(jwt)){
            onLoginFail(servletResponse);
            //調用下面的方法向客戶端返回錯誤信息
            return false;
        }
        logger.info("請求的 Header 中藏有 jwtToken {}", jwt);
        JwtToken jwtToken = new JwtToken(jwt);
        /*
         * 下面就是固定寫法
         * */
        try {

            // 委託 realm 進行登陸認證
            //因此這個地方最終仍是調用JwtRealm進行的認證
            getSubject(servletRequest, servletResponse).login(jwtToken);
            //也就是subject.login(token)
        } catch (Exception e) {
//            e.printStackTrace();
            logger.info(e.getMessage());
            onLoginFail(servletResponse);
            //調用下面的方法向客戶端返回錯誤信息
            return false;
        }

        return true;
        //執行方法中沒有拋出異常就表示登陸成功
    }

    // 從請求中獲取 token
    private String getTokenFromRequest(ServletRequest request) {
        HttpServletRequest req = (HttpServletRequest) request;
        return req.getHeader("Token");
    }

    //登陸失敗時默認返回 401 狀態碼
    private void onLoginFail(ServletResponse response) throws IOException {

        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpResponse.getWriter().write("login error");


    }


}

 

 

 

2.四、JwtToken  類

package com.tg.higo.couponplatform.shiro;

import org.apache.shiro.authc.AuthenticationToken;

public class JwtToken implements AuthenticationToken {


    private String token;

    public JwtToken(String token) {
        this.token = token;
    }


    //yonghu信息
    @Override
    public Object getPrincipal() {
        return token;
    }



    @Override
    public Object getCredentials() {
        return token;
    }


}

 

 

2.五、  重寫Realm  ,定義認證,受權

package com.tg.higo.couponplatform.shiro;

import com.tg.higo.couponplatform.common.utils.JwtUtils;
import com.tg.higo.couponplatform.entity.dto.User;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.Set;

public class AdminShiroRealm extends AuthorizingRealm {




    /**
     * 日誌對象
     */
    protected Logger logger = LoggerFactory.getLogger(AdminShiroRealm.class);


    /*
     * 多重寫一個support
     * 標識這個Realm是專門用來驗證JwtToken
     * 不負責驗證其餘的token(UsernamePasswordToken)
     * */
    @Override
    public boolean supports(AuthenticationToken token) {
        //這個token就是從過濾器中傳入的jwtToken
        return token instanceof JwtToken;
    }



    //認證
    //這個token就是從過濾器中傳入的jwtToken
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        String jwt = (String) token.getCredentials();
        if (!JwtUtils.validToken(jwt)) {
            throw new UnknownAccountException();
        }
        //下面是驗證這個user是不是真實存在的
        User user =  JwtUtils.decodeToken(jwt);//判斷數據庫中username是否存在
//        log.info("在使用token登陸"+username);
        return new SimpleAuthenticationInfo(jwt,jwt,getName());
        //這裏返回的是相似帳號密碼的東西,可是jwtToken都是jwt字符串。還須要一個該Realm的類名

    }




    //    受權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        if (principalCollection == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }
//        User user= JwtUtils.decodeToken(principalCollection.toString());
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            //從數據庫查詢角色
            Set<String> roleSet = new HashSet<>();
            roleSet.add("ghfhgfg");
            authorizationInfo.setRoles(roleSet);
//            //從數據庫查詢權限
            Set<String> permsSet = new HashSet<>();
                permsSet.add("/applet/getZone");
            authorizationInfo.setStringPermissions(permsSet);

        return authorizationInfo;
    }
}

 

 

2.六、 後臺自定義過濾器

package com.tg.higo.couponplatform.shiro;

import com.tg.higo.couponplatform.common.utils.JwtUtils;
import com.tg.higo.couponplatform.entity.dto.User;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 *@Description 後臺自定義過濾器
 **/
public class AdminAuthenticationFilter extends FormAuthenticationFilter {
    private Logger logger= LoggerFactory.getLogger(AdminAuthenticationFilter.class);

    public AdminAuthenticationFilter() {
        super();
    }

    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        Subject subject = getSubject(request,response);
        String url = getPathWithinApplication(request);
        logger.debug("當前用戶正在訪問的 url => " + url);
        if(subject != null && subject.getPrincipal()!=null ) {
            logger.debug("當前用戶" + subject.getPrincipal().toString());
//            logger.debug("getStartTimestamp=" + subject.getSession().getStartTimestamp());
//            logger.debug("getTimeout=" + subject.getSession().getTimeout());
//            logger.debug("getLastAccessTime=" + subject.getSession().getLastAccessTime());
            logger.debug("isAuthenticated=" + subject.isAuthenticated());
        }
        logger.debug("isPermitted=" + subject.isPermitted(url));
        return subject.isPermitted(url);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse res = (HttpServletResponse)response;
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setHeader("Content-Type","application/json");
        res.setStatus(HttpServletResponse.SC_OK);
        res.setCharacterEncoding("UTF-8");
        Subject subject = getSubject(request,response);
        Object currentUser = JwtUtils.decodeToken((String) subject.getPrincipal());
//        ApiResult apiResult;
        logger.debug("isAuthenticated=" + subject.isAuthenticated());
        logger.debug("instanceof=" + (currentUser instanceof User));
        logger.debug("isPermitted=" + subject.isPermitted(getPathWithinApplication(request)));
        PrintWriter writer = res.getWriter();
        logger.info("subject.isAuthenticated:",subject.isAuthenticated());
        logger.info("currentUser instanceof User:",currentUser instanceof User);
        if(subject.isAuthenticated() && currentUser instanceof User){
            writer.write("沒有權限,請聯繫管理員受權。");
//            apiResult=new ApiResult(403,"沒有權限,請聯繫管理員受權。");
        }else{
            writer.write("登陸超時,請從新登陸。");
//            apiResult=new ApiResult(401,"登陸超時,請從新登陸。");
        }

//        writer.write(JSON.toJSONString(apiResult));
        writer.close();
        return false;
    }
}

 

 

2.七、 Shiro相關配置

package com.tg.higo.couponplatform.config;

import com.tg.higo.couponplatform.shiro.*;
import org.apache.shiro.authz.permission.PermissionResolver;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;


/**
 * @Description Shiro相關配置
 **/
@Configuration
public class ShiroConfig {


    @Bean
    public Realm adminShiroRealm() {
        AdminShiroRealm adminShiroRealm = new AdminShiroRealm();
        //將自定義的權限匹配器注入到自定義 Realm 中
//        adminShiroRealm.setPermissionResolver(urlPermissionResolver());
//        //設置解密規則
//        adminShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return adminShiroRealm;
    }

//    @Bean
//    public PermissionResolver urlPermissionResolver(){
//        return new UrlPermissionResolver();
//    }

    @Bean
    public CacheManager cacheManager() {
        return new MemoryConstrainedCacheManager();
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
//        sm.setAuthenticator(modularRealmAuthenticator());
        List<Realm> realms = new ArrayList<>();
        realms.add(adminShiroRealm());
//        realms.add(userShiroRealm());
//        realms.add(freeRealm());
        sm.setCacheManager(cacheManager());
        //注入自定義sessionManager
//        sm.setSessionManager(sessionManager());
        sm.setRealms(realms);


        // 關閉 ShiroDAO 功能
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // 不須要將 Shiro Session 中的東西存到任何地方(包括 Http Session 中)
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        sm.setSubjectDAO(subjectDAO);
        //禁止Subject的getSession方法
        sm.setSubjectFactory(subjectFactory());

        return sm;
    }


    @Bean
    public JwtDefaultSubjectFactory subjectFactory() {
        return new JwtDefaultSubjectFactory();
    }


    public AdminAuthenticationFilter adminAuthenticationFilter(){
        return new AdminAuthenticationFilter();
    }


    public JwtFilter jwtFilter() {
        return new JwtFilter();
    }

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //配置不會被攔截的連接,順序判斷
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/*.ico", "anon");
        filterChainDefinitionMap.put("/static/js/**", "anon");
        filterChainDefinitionMap.put("/static/css/**", "anon");
        filterChainDefinitionMap.put("/static/fonts/**", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");
        //接口文檔
        filterChainDefinitionMap.put("/doc.html", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources", "anon");
        filterChainDefinitionMap.put("/api-docs", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/swagger.json", "anon");
        //後臺登陸
        filterChainDefinitionMap.put("/admin/login", "anon");
        // 批量同步密碼接口
        filterChainDefinitionMap.put("/api/test/updateMobile", "anon");

        //退出登陸
        filterChainDefinitionMap.put("/**/logout", "anon");
        //authc:全部url必須經過認證才能訪問,anon:全部url均可以匿名訪問
        filterChainDefinitionMap.put("/**", "jwt,adminFilter");


        shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
        //自定義過濾器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("jwt", jwtFilter());
        filterMap.put("adminFilter", adminAuthenticationFilter());
        shiroFilter.setFilters(filterMap);

        return shiroFilter;
    }

    /**
     * Shiro生命週期處理器 * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


}

 

完成

 

 

3、登陸接口

/**
     * 帳號登陸
     *
     * @param account:數字和下劃線
     * @param password:2次md5加密
     * @return
     */
    @RequestMapping("/login")
    @ResponseBody
    public JSONObject login(String account, String password) {
        return accountInfoService.login(account, password);
    }


   //帳號登陸
    public JSONObject login(String account, String password) {

        //判斷參數
        if (StringUtil.isBlank(account) || StringUtil.isBlank(password)) {
            return ReturnEntity.missParams();
        }
        //獲取帳號
        AccountInfoDto accountInfoDto = AccountInfoDao.selectByAccount(account);
        if (accountInfoDto == null) {
            return ReturnEntity.fail("帳號有誤");
        }

        if (!accountInfoDto.getPassword().equals(password)) {
            return ReturnEntity.fail("密碼錯誤");
        }

        if (accountInfoDto.getState() == User.closeState) {
            return ReturnEntity.fail("帳號已經禁用");
        }

        //獲取返回token
        User user = new User();
        user.setAccount(accountInfoDto.getAccount());
        user.setRole(accountInfoDto.getRole());
        if (user.getRole() == User.orgRole) {
            user.setOrgId(accountInfoDto.getOrgId());
        } else {
            user.setOrgId("0");
        }



        String   token = JwtUtils.encodeToken(user);
        //設置好token,後來會在全局處理的時候放入響應裏
        req.setAttribute("token", token);
        JSONObject data = new JSONObject();
        data.put("token", token);
//        data.put("expire", JwtUtils.VALID_SECONDS);
//        data.put("role", user.getRole());

        return ReturnEntity.success(data, "登陸成功");
    }

 

 

完成

 

測試:

 

後續請求帶token

就能夠了

相關文章
相關標籤/搜索