SpringBoot2.0,Thymeleaf与Shiro整合

前言

在2017年年底发布的一篇博客中介绍了前后端分离的场景下,springboot与shiro整合,时隔一年多,springboot早已升级到2.X,前后端分离也慢慢回归一体化,于是有了这样一篇博客,本文主要介绍在前后端不分离的场景下,springboot2与shiro以及thymeleaf模板引擎的整合。另外,在之前的文章中很多评论提到的问题,在本文也会一并解答。

正文

先介绍本项目中使用的主要依赖。该项目使用SpringBoot 2.1版本,权限控制使用Shiro 1.4版本,WEB层为springboot-web,持久层使用Mybatis-Plus 2.3.3版本,为了便于demo快速启动,数据库使用了H2,实际使用时可以切换到MySQL。为了便于session共享,Shiro的sessionId保存于redis中,使用了开源框架shiro-redis 3.1.0版本。

项目仍然是多模块项目,其中core为核心模块,包括公用数据模型,工具类,常量,通用操作类等等。

service模块是公共服务模块,其中包含实体类,相关服务接口与实现,Shiro基础配置等。

portal是网站前台模块,作为前台的启动模块可以打war包发布,因此包含各种配置文件,以及配置类,业务上包括用户登录相关的控制器,登录页面等,在实际项目中,前台网站相关的控制器以及页面都应该在此模块中。

admin是网站后台管理模块,也可单独打包发布,包括各种配置文件。在实际项目中,各种基础数据管理的控制器以及页面应该在此模块中。

generator模块用作Mybatis-Plus的代码生成。

下面着重介绍Shiro相关配置。

使用Shiro必须自定义Realm,先上代码

package com.sst.service.common.shiro;

import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.sst.core.util.PasswordUtil;
import com.sst.service.system.entity.SysUser;
import com.sst.service.system.service.ShiroService;
import com.sst.service.system.service.SysUserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Collection;


/**
 * @Author: Ian
 */
public class CustomShiroRealm extends AuthorizingRealm {

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private ShiroService shiroService;

    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SysUser user = (SysUser) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(shiroService.getUserPerms(user));
        info.setRoles(shiroService.getUserRoles(user));
        return info;
    }

    /**
     * 登录认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        //获取用户账号
        String username = token.getUsername();

        String password = String.valueOf(token.getPassword());
        //查询用户信息
        SysUser sysUser = sysUserService.selectOne(new EntityWrapper<SysUser>().eq("username", username));
        //账号不存在、密码错误
        if (sysUser == null || !PasswordUtil.validatePassword(password, sysUser.getPassword(), sysUser.getSalt())) {
            throw new IncorrectCredentialsException("用户名或密码不正确");
        }
        //账号锁定
        if (sysUser.getStatus() == 0) {
            throw new LockedAccountException("账号已被锁定,请联系管理员");
        }
        //清除该用户以前登录时保存的session,强制退出
//        removeOldSession(username);

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(sysUser, password, getName());
        return info;
    }

    private void removeOldSession(String username) {
        DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
        DefaultWebSessionManager sessionManager = (DefaultWebSessionManager) securityManager.getSessionManager();
//        获取当前已登录的用户session列表
        Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();
        SysUser temp;
        for (Session session : sessions) {

            Object attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (attribute == null) {
                continue;
            }

            temp = (SysUser) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
            if (username.equals(temp.getUsername())) {
                sessionManager.getSessionDAO().delete(session);
            }
        }
    }
}

在realm中我们重写doGetAuthorizationInfo方法来实现对当前登录用户授权的业务逻辑,重写doGetAuthenticationInfo来实现登录认证的业务逻辑。相信使用过Shiro框架的同学都非常熟悉以上代码,值得注意的是,我们保存在数据库中的密码是加密过的密码,同时还存储了盐值来加强密码的安全性,具体的加密方式可以在源码中查看PasswordUtil工具类。如果我们想实现当一个用户登录时,将其他用相同账号登录的用户踢下线,那么可以解除removeOldSession方法的注释,在用户认证成功后删除存储在redis中的其他同账号的session数据。

自定义SessionManager

import com.sst.core.constant.CommonConstants;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * @Author: Ian
 * @Date: 2019/4/10
 */
public class CustomSessionManager extends DefaultWebSessionManager {


    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public CustomSessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //改变默认id获取顺序 优先获取请求路径中的sessionId
        String id = WebUtils.toHttp(request).getParameter(CommonConstants.URL_TOKEN);
        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }
}

我们重写了获取sessionId的逻辑。Shiro默认优先从cookie中获取sessionId,而我们在此改为优先从请求参数中获取sessionId,同时自定义了参数的名称。由于cookie是无法跨域的,我们这样改动使得用户只要有sessionId即使跨域也可认证成功,也就支持了前后端分离,同时也便于第三方调用。在上一篇文章中,我们是优先从请求头中获取sessionId,此处逻辑可以按需求处理。

在某些情况下(重定向等),Shiro会给页面的url加上;JSESSIONID=xxx的后缀,这个默认行为会导致前端页面url错误,比如使用thymeleaf的@{/}获取项目根路径时。因此我们自定义了ShiroHttpServletResponse和ShiroFilterFactoryBean类来避免Shiro重写url。

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.mgt.FilterChainManager;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.springframework.beans.factory.BeanInitializationException;

import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;


public class CustomShiroFilterFactoryBean extends ShiroFilterFactoryBean {

    @Override
    public Class getObjectType() {
        return CustomSpringShiroFilter.class;
    }

    @Override
    protected AbstractShiroFilter createInstance() throws Exception {

        org.apache.shiro.mgt.SecurityManager securityManager = getSecurityManager();
        if (securityManager == null) {
            String msg = "SecurityManager property must be set.";
            throw new BeanInitializationException(msg);
        }

        if (!(securityManager instanceof WebSecurityManager)) {
            String msg = "The security manager does not implement the WebSecurityManager interface.";
            throw new BeanInitializationException(msg);
        }
        FilterChainManager manager = createFilterChainManager();

        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);

        return new CustomSpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
    }

    private static final class CustomSpringShiroFilter extends AbstractShiroFilter {

        protected CustomSpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
            super();
            if (webSecurityManager == null) {
                throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
            }
            setSecurityManager(webSecurityManager);
            if (resolver != null) {
                setFilterChainResolver(resolver);
            }
        }

        @Override
        protected ServletResponse wrapServletResponse(HttpServletResponse orig, ShiroHttpServletRequest request) {
            return new CustomShiroHttpServletResponse(orig, getServletContext(), request);
        }
    }
}
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.servlet.ShiroHttpServletResponse;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;

public class CustomShiroHttpServletResponse extends ShiroHttpServletResponse {

    public CustomShiroHttpServletResponse(HttpServletResponse wrapped, ServletContext context, ShiroHttpServletRequest request) {
        super(wrapped, context, request);
    }

    /**
     * Return the specified URL with the specified session identifier suitably encoded.
     *
     * @param url       URL to be encoded with the session id
     * @param sessionId Session id to be included in the encoded URL
     * @return the url with the session identifer properly encoded.
     */
    @Override
    public String toEncoded(String url, String sessionId) {

        if ((url == null) || (sessionId == null))
            return (url);

        String path = url;
        String query = "";
        String anchor = "";
        int question = url.indexOf('?');
        if (question >= 0) {
            path = url.substring(0, question);
            query = url.substring(question);
        }
        int pound = path.indexOf('#');
        if (pound >= 0) {
            anchor = path.substring(pound);
            path = path.substring(0, pound);
        }
        StringBuilder sb = new StringBuilder(path);
        //重写toEncoded方法,注释掉这几行代码就不会再生成JESSIONID了。
//        if (sb.length() > 0) { // session id param can't be first.
//            sb.append(";");
//            sb.append(DEFAULT_SESSION_ID_PARAMETER_NAME);
//            sb.append("=");
//            sb.append(sessionId);
//        }
        sb.append(anchor);
        sb.append(query);
        return (sb.toString());

    }
}

以上两段代码取自网络,源头不可考,但亲测有效。

我们希望在html模板上从session方便的获取用数据,因此需要将用户信息保存在session中,无论是使用用户密码登录后,还是在使用Shiro的rememberMe功能时。因此有了一下代码。

import com.sst.core.constant.CommonConstants;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpSession;

/**
 * @Author: Ian
 * @Date: 2019/4/9
 */
public class UserSessionFilter extends AccessControlFilter {
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        if (subject == null) {
            // 没有登录
            return false;
        }
        HttpSession session = WebUtils.toHttp(request).getSession();
        Object loginUser = session.getAttribute(CommonConstants.SESSION_USER_INFO);
        if (loginUser == null && subject.getPrincipal() != null) {
            session.setAttribute(CommonConstants.SESSION_USER_INFO, subject.getPrincipal());
        }
        return true;
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return true;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        return true;
    }
}

在创建了以上自定义类后,我们需要通过配置让Shiro使用。由于portal和admin两个系统都需要Shiro配置,为了方便复用,创建了BaseShiroConfig类,而在portal和admin各自有ShiroConfig继承于BaseShiroConfig。

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.sst.core.constant.CommonConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * @Author: Ian
 * @Date: 2019/4/8
 */
@Slf4j
public class BaseShiroConfig {

    private static final String SECRET_KEY = "sst1234";

    private static final String COOKIE_PATH = "/";

    @Autowired
    private RedisProperties redisProperties;

    /**
     * 自定义的Realm
     */
    @Bean(name = "customShiroRealm")
    public CustomShiroRealm customShiroRealm() {
        CustomShiroRealm customShiroRealm = new CustomShiroRealm();
        return customShiroRealm;
    }


    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customShiroRealm());
        securityManager.setRememberMeManager(rememberMeManager());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(redisCacheManager());
        return securityManager;
    }


    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisProperties.getHost());
//        redisManager.setPassword(redisProperties.getPassword());
        redisManager.setDatabase(redisProperties.getDatabase());
        redisManager.setPort(redisProperties.getPort());
        return redisManager;
    }


    @Bean
    public RedisCacheManager redisCacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }


    @Bean
    public DefaultWebSessionManager sessionManager() {
        CustomSessionManager sessionManager = new CustomSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        sessionManager.setSessionIdCookieEnabled(true);
        sessionManager.setSessionIdCookie(simpleCookie());
        return sessionManager;
    }

    /**
     * 保存sessionId的cookie
     *
     * @return
     */
    @Bean
    public SimpleCookie simpleCookie() {
        SimpleCookie simpleCookie = new SimpleCookie();
        simpleCookie.setName(CommonConstants.SHIRO_COOKIE);
        simpleCookie.setPath(COOKIE_PATH);
        return simpleCookie;
    }

    /**
     * 记住我的cookie对象;
     *
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie() {
        SimpleCookie simpleCookie = new SimpleCookie();
        simpleCookie.setName(CommonConstants.SHIRO_REMEMBER_ME);
        //<!-- 记住我cookie生效时间30天 ,单位秒;-->
        simpleCookie.setMaxAge(259200);
        simpleCookie.setPath(COOKIE_PATH);
        return simpleCookie;
    }

    /**
     * cookie管理对象;
     * rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
     *
     * @return
     */
    @Bean
    public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        byte[] bytesOfMessage = null;
        MessageDigest md = null;
        try {
            bytesOfMessage = SECRET_KEY.getBytes("UTF-8");
            md = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
            log.error(e.getMessage(), e);
        }
        byte[] b = md.digest(bytesOfMessage);
        //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(b);
        return cookieRememberMeManager;
    }

    /**
     * 开启shiro注解
     *
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor
                = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

}

其中RedisSessionDao引自开源框架shiro-redis,其管理了sessionId在redis中的增删改查,sessionId在redis中默认过期时间为1800秒,每当登录用户访问系统时,过期时间会刷新,也就是说只有当用户在1800秒内没有任何访问时,session才会过期。

上面代码中simpleCookie配置了Shiro在cookie中存储sessionId使用的key,过期时间,以及PATH路径,由于我们session过期依赖于redis,因此cookie设定为永久有效,在重新登录后cookie会被新的值覆盖。为了使得同域名下的系统登录状态互通,我们给PATH配置为“/”即根路径,使得在portal登录后,进入admin系统无需再次登录。

再来看看portal模块下面的ShiroConfig类。

import com.sst.service.common.shiro.BaseShiroConfig;
import com.sst.service.common.shiro.CustomShiroFilterFactoryBean;
import com.sst.service.common.shiro.UserSessionFilter;
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.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;


/**
 * @Author: Ian
 * @Date: 2019/4/8
 */
@Configuration
public class ShiroConfig extends BaseShiroConfig {

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map<String, Filter> filters = new HashMap<>();
        filters.put("userSession", new UserSessionFilter());
        shiroFilterFactoryBean.setFilters(filters);

        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/login", "anon");
        filterMap.put("/login.htm", "anon,userSession");
        filterMap.put("/**/*.css", "anon");
        filterMap.put("/**/*.js", "anon");
        filterMap.put("/**/*.html", "anon");
        filterMap.put("/img/**", "anon");
        filterMap.put("/fonts/**", "anon");
        filterMap.put("/**/favicon.ico", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/", "user,userSession");
        filterMap.put("/sysWebsiteInfo/**", "anon");
        //只有登录才能访问用authc,登录和记住我都能访问用user
        filterMap.put("/**", "user,userSession");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        shiroFilterFactoryBean.setLoginUrl("/login.htm");
        return shiroFilterFactoryBean;
    }

}

shiroFilter这个Bean中我们使用了自定义的ShiroFilterFactoryBean,并在过滤器链中加入了自定义的UserSessionFilter过滤器。在需要从session取用户信息的路由中进行了配置。需要注意的是只有使用用户名密码登录才能访问的路径用authc,而登录和rememberMe都能访问用user。由于前后端不分离,登录页直接写上页面路由,鉴权未通过的请求将会直接重定向到登录页。

控制层的用户登录和登出接口代码如下。

    /**
     * 用户登录
     *
     * @param sysUser
     * @return
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST,
            consumes = "application/json;charset=UTF-8", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public ResponseDomain doLogin(@RequestBody SysUser sysUser) {
        Subject subject = SecurityUtils.getSubject();
        String username = sysUser.getUsername();
        UsernamePasswordToken token = new UsernamePasswordToken(username, sysUser.getPassword(), sysUser.isRememberMe());
        try {
            subject.login(token);
        } catch (AuthenticationException e) {
            log.error(e.getMessage(), e);
            token.clear();
            return ResponseDomain.getFailedResponse().setResultDesc(e.getMessage());
        }

        return ResponseDomain.getSuccessResponse().setResultDesc("登录成功");
    }

    /**
     * 退出登录
     *
     * @return
     */
    @RequestMapping(value = "/logout", method = RequestMethod.POST)
    @ResponseBody
    public ResponseDomain logout() {
        SecurityUtils.getSubject().logout();
        return ResponseDomain.getSuccessResponse();
    }

Shiro相关的核心代码已经全部贴出。

下面测试下我们的需求是否都已实现。

由于springboot-devtool会导致Bean转换失败的BUG(参考https://www.cnblogs.com/wobuchifanqie/p/9908243.html),因此调试时使用tomcat发布。

由于使用内存数据库,启动redis后项目就可以运行了。

访问portal模块登录页,地址http://localhost:9001/portal/login.htm

点击登录后。

可以看到,“管理员”这个名称已经顺利从session中获取。

<p class="name" th:text="${session.loginUser.realName}+' 已登录'"></p>

点击后台管理按钮,跳转到后台首页http://localhost:9002/admin/index.htm

这说明登录状态以及session都已共享成功。

 

问题

在前后端分离的SpringBoot项目中集成Shiro权限框架》这篇博客发出后,很多同学产生了疑问并在文章后留言,作者由于时间有限并没有全部回复,在次做一个总结。

1.前后端分离跨域怎么解决?

后台需要支持跨域访问,最简单的解决方法就是加一个CORS过滤器,具体代码可以自行搜索,也可以直接引用com.thetransactioncompany的cors-filter,然后进行相应配置,这个过滤器一定要在Shiro的过滤器之前执行。

2.如何管理session过期?

上面文中已经有具体说明,简单来说就是让redis管理,redis中存储的session过期了,该用户的session就过期了。

3.如何保证安全性,token被盗用怎么办?

可能最安全的方法就是不要让token被截获,那么就得使用https协议访问后台。如果没有那么高的安全性需求,可以在前端对sessionId(token)进行加密保存和传递,加密可以动态加密,就是密钥从后台请求获取,密钥定时刷新。

4.为什么不用JWT?

JWT作为token由于有签名校验机制,安全性较高,但复杂度也随之升高,要更安全还是更快捷,还要根据项目需求来定。

源码

https://github.com/rewindian/springboot2-shiro-thymeleaf

 

  • 5
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值