spring boot 項目總結1-shiro 使用

(shiro 整體架構)

 

Spring與Shiro整合:

一般步驟:

  • 在web.xml文件中配置shiro的過濾器
  • 在對應的Spring配置文件中配置與之對應的filterChain(過慮鏈兒)
  • 配置安全管理器,注入自定義的reaml
  • 配置自定義的reaml

我們項目的配置文件,是以代碼形式寫的,沒有用配置文件。

如何讓spring boot  認識它們呢?


  加上這個註解  項目啓動就會掃描  相當於這裏面寫的方法 變量  都放在spring boot的啓動類裏了。

shiro  比較重要的配置項:

ShiroFilterFactoryBean 

securityManager  安全管理器

靜態資源不攔截:

anon其實就是shiro內置的一個過濾器

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
       // Shiro的核心安全接口,這個屬性是必須的
       shiroFilterFactoryBean.setSecurityManager(securityManager);

        //自定過濾器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("authc", new ShiroLoginFilter());
        //filterMap 裝配進shiroFilterFactoryBean
        shiroFilterFactoryBean.setFilters(filterMap);

        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/user/login", "anon");
        ......
        filterChainDefinitionMap.put("/sys/sysNontaxparams/getByCode", "anon");//非稅參數
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

過濾器:

anon:例子/admins/**=anon 沒有參數,表示可以匿名使用。
authc:例如/admins/user/**=authc表示需要認證(登錄)才能使用,FormAuthenticationFilter是表單認證,沒有參數
perms:例子/admins/user/**=perms[user:add:*],參數可以寫多個,多個時必須加上引號,並且參數之間用逗號分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],當有多個參數時必須每個參數都通過才通過,想當於isPermitedAll()方法。
user:例如/admins/user/**=user沒有參數,表示必須存在用戶, 身份認證通過或通過記住我認證通過的可以訪問,當登入操作時不做檢查

登錄的驗證:

步驟:
賬戶、密碼認證:
1、創建Subject主體;
2、將從前端得到的賬號,密碼存放到Token中;
3、再使用subject.login(token)提交認證;
4、UserRealm的doGetAuthenticationInfo認證方法中,通過從token中拿到賬戶號,從數據查詢出用戶的密碼和鹽;new 一個SimpleAuthenticationInfo ,將賬戶名、密碼、鹽(使用ByteSource.Util.bytes()方法轉換爲ByteSource類型)、當前Realm的name封裝進去。
4、返回SimpleAuthenticationInfo對象(該對象會傳入憑證匹配器中),憑證匹配器HashedCredentialsMatcher 會根據Token(你前臺登錄時的明文賬號密碼,和數據庫查詢出來的加密後的密碼比較)

 

當然,你還得讓shiro框架認識這個real,配置文件當中加上一筆:

    @Bean
    public Realm realm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setAuthenticationTokenClass(PersonnelPasswordToken.class);
        return userRealm;
    }

我們自定義了PersonelPasswordToken,封裝了一些擴展信息。

token 相關有2個概念:credentials,principal 

principal 我理解就是用戶名

credentials這個屬性,在UsernamePasswordToken中其實是個Object,查看源代碼,getCredentials()方法返回的就是password

源代碼,見圖:

故,若要正確得到UsernamePasswordToken的password,可以將credentials轉爲char[]再String.valof()方法獲得String。

登錄認證的代碼:


public class UserRealm extends AuthorizingRealm {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        BasPersonnelDao basPersonnelDao = ApplicationContextRegister.getBean(BasPersonnelDao.class);
        BasBusinessofficesService basBusinessofficesService = ApplicationContextRegister.getBean(BasBusinessofficesService.class);
        BasChargeagencyService basChargeagencyService = ApplicationContextRegister.getBean(BasChargeagencyService.class);
        BasAdmdivDao basAdmdivDao = ApplicationContextRegister.getBean(BasAdmdivDao.class);
        SysNontaxparamsService sysNontaxparamsService = ApplicationContextRegister.getBean(SysNontaxparamsService.class);
//取得token
        PersonnelPasswordToken token = (PersonnelPasswordToken) authenticationToken;
        String username = (String) token.getPrincipal();
        String password = new String((char[]) token.getCredentials());

         ......

        BasPersonnel basPersonnel = null;
        try {
//從dao查找用戶
            List<BasPersonnel> basPersonnels = null;
            if (StringUtils.isEmpty(groupid))//財政用戶
                basPersonnels = basPersonnelDao.getAllByUsercodeAndYearAndAdmdivcodeAndPerunittype(username, year, admdivcode, 1);
            else {//單位用戶
                if (groupid.equals("ptYh")) {
                  ......
                } else {
                    basPersonnels = basPersonnelDao.getAllByUsercodeAndYearAndAdmdivcodeAndPerunittypeAndGroupid(username, year, admdivcode, token.getPerunittype(), groupid);
                }
            }
            basPersonnel = basPersonnels.get(0);
        } catch (Exception e) {
            logger.warn(e.getMessage());
            throw new ReturnException("無此用戶!");
        }
        if (basPersonnel == null) {
            throw new ReturnException("無此用戶!");
        }
      //驗證密碼
        String md5Psw = MD5Utils.encrypt(password).toUpperCase();
        if (!basPersonnel.getPassword().toUpperCase().equals(md5Psw)) {
            SysNontaxparams byCode = sysNontaxparamsService.getByCode("901001", year, admdivcode);
            String paramvalue = byCode.getParamvalue();
            if (!paramvalue.equals(password)) {
                throw new ReturnException("密碼錯誤!");
            }
            logger.warn("用戶:{},超級密碼登錄!", username);
        } else {
            logger.info("用戶:{},登錄成功!", username);
        }
        //加載用戶 Roleids
        try {
            List<String> strings = basPersonnelDao.getRoleIDSByUserId(basPersonnel.getGuid());
            String roles = "";
            for (String string : strings) {
                roles += "'" + string + "',";
            }
            if (StringUtils.isEmpty(roles)) {
                basPersonnel.setRoleids("");
            } else {
                roles = roles.substring(0, roles.length() - 1);
                basPersonnel.setRoleids(roles);
            }
        } catch (Exception e1) {
            logger.warn("用戶登錄成功,權限未加載!");
        }
        try {
            basPersonnel.setAdmdivname(basAdmdivDao.getAdmName(basPersonnel.getAdmdivcode()).get(0));
        } catch (Exception e) {
            logger.warn("區劃名稱加載異常!");
        }
        try {
            switch (basPersonnel.getPerunittype()) {
                case 1:
                    //財政用戶
                    BasBusinessoffices basBusinessoffices = basBusinessofficesService.get(basPersonnel.getGroupid());
                    basPersonnel.setOfficecode(basBusinessoffices.getOfficecode());
                    basPersonnel.setOfficename(basBusinessoffices.getOfficename());
                    break;
                case 2:
                    //單位用戶
                    BasChargeagency basChargeagency = basChargeagencyService.get(basPersonnel.getGroupid());
                    basPersonnel.setOfficecode(basChargeagency.getAgencycode());
                    basPersonnel.setOfficename(basChargeagency.getAgencyname());
                    try {
                        String paramvalue = sysNontaxparamsService.getByCode("404001", year, admdivcode).getParamvalue();
                        if (paramvalue != null && paramvalue.equals("1")) {
                            basPersonnel.setIshall(basChargeagency.getIshall());
                        }
                    } catch (Exception e) {
                        logger.warn("大廳模式非稅參數讀取異常");
                    }
                    break;
            }
        } catch (Exception e) {
            logger.warn("用戶單位名稱加載異常->用戶GUID:{}", basPersonnel.getGuid());
        }

        //系統管理員 加載客戶端類型
        if (basPersonnel.getUsercode().equals("_SYSADMIN")) {
            basPersonnel.setClientType("4");
        } else {
            basPersonnel.setClientType(basPersonnel.getPerunittype().toString());
        }

        basPersonnel.setMack(mack);

        //賬套ID
        basPersonnel.setSetid("1234");
//構造SimpleAuthenticationInfo,返回
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(basPersonnel, password, getName());
        return info;
    }

}

登錄的時候,獲取sessionId 作爲token,返回給客戶端。

用戶登錄成功後,會從服務端獲取一個token(sessionId充當)。以後每次請求都會帶上這個token.

當然,每次請求也必須覈對這個token,否則,無法訪問。

退出登錄:

@GetMapping("/logout")
public R logout() {
    SecurityUtils.getSubject().logout();
    return R.ok();
}

會話管理:

      頂層組件SecurityManager直接繼承了SessionManager,且提供了SessionsSecurityManager實現直接把會話管理委託給相應的SessionManager。Shiro提供了兩個實現:DefaultSecurityManager及DefaultWebSecurityManager。

        shiro默認使用 ServletContainerSessionManager 來做 session 管理,它是依賴於瀏覽器的 cookie 來維護 session 的,調用 storeSessionId  方法保存sesionId 到 cookie中
 *      爲了支持無狀態會話,我們就需要繼承 DefaultWebSessionManager

另外,默認提供的sessionDAO是memorySessionDAO。

會話管理配置:

配置文件中來一段:

   @Bean
    public SessionManager sessionManager() {
        TokenSessionManager sessionManager = new TokenSessionManager();
        sessionManager.setSessionDAO(sessionDAO());
        try {
            Long property = Long.parseLong(Objects.requireNonNull(env.getProperty("system.timeout")));
            log.info("會話過期時間-->{}s", property);
            sessionManager.setGlobalSessionTimeout(property * 1000);
        } catch (Exception e) {
            sessionManager.setGlobalSessionTimeout(3600 * 1000);
            log.warn("未找到會話過期時間配置 默認爲3600s");
        }

        return sessionManager;
    }

    @Bean
    public SessionDAO sessionDAO() {
        if (redistype.equals("1")) {
            return redisSessionDAO();
        } else {
            return new MemorySessionDAO();
        }
    }

ps:shiro 的sessionDao接口:

public interface SessionDAO {
    Serializable create(Session var1);
 
    Session readSession(Serializable var1) throws UnknownSessionException;
 
    void update(Session var1) throws UnknownSessionException;
 
    void delete(Session var1);
 
    Collection<Session> getActiveSessions();
}

sessionManager 的應用:

獲取在線用戶:

    @ApiOperation(value = "在線用戶列表")
    @GetMapping("/onlineUserList")
    public R onlineUserList(Integer pageSize, Integer pageIndex) {
        Collection<Session> sessions = sessionDAO.getActiveSessions();
        List<JSONObject> objects = sessions.stream().map(session -> {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("sessionId", session.getId());
            jsonObject.put("timeout", session.getTimeout());
            jsonObject.put("ip", session.getHost());
            jsonObject.put("lastAccessTime", DateUtils.format(session.getLastAccessTime(), DateUtils.DATE_TIME_PATTERN));
            jsonObject.put("startTimestamp", DateUtils.format(session.getStartTimestamp(), DateUtils.DATE_TIME_PATTERN));
            SimplePrincipalCollection attribute = (SimplePrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            BasPersonnel basPersonnel = (BasPersonnel) attribute.getPrimaryPrincipal();
            jsonObject.put("username", basPersonnel.getUsername());
            jsonObject.put("usercode", basPersonnel.getUsercode());
            jsonObject.put("officename", basPersonnel.getOfficename());
            jsonObject.put("officecode", basPersonnel.getOfficecode());
            jsonObject.put("perunittype", basPersonnel.getPerunittype() == 0 ? "財政用戶" :
                    basPersonnel.getPerunittype() == 1 ? "單位用戶" : "銀行用戶");
            return jsonObject;
        }).collect(Collectors.toList());

        int size = objects.size();
        //分頁
        int i = (pageIndex - 1) * pageSize;
        int i1 = i + pageSize;
        objects = objects.subList(i > size ? size : i, i1 > size ? size : i1);

        Map<String, Object> map = new HashMap<>();
        map.put("data", objects);
        Map<String, Object> pageInfo = new HashMap<>();
        pageInfo.put("totalCount", size);
        map.put("pageInfo", pageInfo);
        return R.ok(map);
    }

退出或者強制用戶下線:

    @ApiOperation(value = "用戶下線")
    @PostMapping("/logoutBysessionId")
    public R logoutBysessionId(@RequestBody String sessionId) {
        for (Session session : sessionDAO.getActiveSessions()) {
            if (session.getId().equals(sessionId)) {
                sessionDAO.delete(session);
                return R.ok();
            }
        }
        return R.error("未找到對應會話");
    }

 如果我們想在強制讓某用戶登出系統,另外一個辦法是隻需要session.setTimeout(0)即可。

我們借用sessionid充當會話token,問題 如何判斷傳遞的sessionId 是合法的?借用getSession方法?

 SessionManager 接口
SessionManager 接口是Shiro所有會話管理器的頂級接口。在此接口中聲明瞭兩個方法Session start(SessionContext context);和Session getSession(SessionKey key) throws SessionException;。

Session start(SessionContext context);方法,基於指定的上下文初始化數據啓動新會話。
Session getSession(SessionKey key) throws SessionException; 根據指定的SessionKey檢索會話,如果找不到則返回null。如果找到了會話,但會話但無效(已停止或已過期)則拋出SessionException異常。

public class TokenSessionManager extends DefaultWebSessionManager {
    public TokenSessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String tokenId = WebUtils.toHttp(request).getHeader("X-Token");
        if (StringUtils.isNotEmpty(tokenId)) {
            return tokenId;
        }
        tokenId = request.getParameter("token");
        if (StringUtils.isNotEmpty(tokenId)) {
            return tokenId;
        }
        //否則按默認規則從cookie取sessionId
        return super.getSessionId(request, response);
    }
}

由於我們是前後端分離的模式,所以要自定義ToeknSessionManager,根據傳入的token,檢索會話,如果沒找到,就會拋出例外,會話會中斷?

會話拋出錯誤,最後落腳點還得通過過濾器處理?過濾器當然需要在配置文件中註冊,如果會話失敗,我們這裏給出提示?

public class ShiroLoginFilter extends FormAuthenticationFilter {

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(JSONObject.toJSONString(R.other(501, "請登錄")));
        return false;
    }
}

前端根據這裏的代碼(501)跳轉到特定頁面?

如果沒有token,或者傳入的token錯誤,那麼就認爲會話無效,就會執行ShiroLoginFilter中的onAccessDenied方法?

改進點:oken本身是否需要加密簽名呢?另外,我們項目沒有用shiro作爲授權體系,是否能採用shiro授權體系呢?

ps:

Shiro 中的 SessionManager

Shiro【授權、整合Spirng、Shiro過濾器】

Spring+Shiro權限管理 (一) 使用MD5+salt(鹽)加密、認證

前後分離,使用自定義token作爲shiro認證標識,實現springboot整合shiro

shiro使用(使用token)

shiro的FormAuthenticationFilter的作用

 

 

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