SpringBoot 整合Shiro 实现前后端分离


  最近着手开发一个SpringBoot + Shiro 的后台框架, 设计到前后端分离,需要跨域请求,但是登陆成功之后再进行其他操作总是提示未登录 重定向跳转到unlogin页面(前后分离模式,重定向也要改成json返回,后续贴出代码)

1、修改登陆方法

登陆之后返回sessionId给前端

		//获取subject对象
		Subject subject = SecurityUtils.getSubject();
		//封装用户数据
		LoginAuthToken token = new LoginAuthToken(username, password,rememberMe,userType);

		try {
			//执行Shiro配置的拦截方法
			subject.login(token);
			//登录失败:用户名不存在
		} catch (UnknownAccountException e) {
			e.printStackTrace();
			return new ResultVoFailure("用户名不存在");
			//登录失败:密码错误
		} catch (IncorrectCredentialsException e) {
			return new ResultVoFailure("密码错误");
		}
		return new ResultVoSuccess("登录成功",subject.getSession().getId());

2、重写SessionManager对象

重写sessionManager对象处理session

package com.pengheng.config.shiro;
 
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
 
/**
 * Created by Palerock
 */
public class SessionManager extends DefaultWebSessionManager {
    private static final String  TOKEN= "token";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public SessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String id = WebUtils.toHttp(request).getParameter(TOKEN);
        //如果请求信息中有 TOKEN 则其值为sessionId
        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);
        }
    }
}

3、修改ShiroConfig配置引入自定义sessionManger

package com.pengheng.config.shiro;

import com.pengheng.config.shiro.filter.MyFormAuthenticationFilter;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
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.mindrot.jbcrypt.BCrypt;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    /**
     * rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
     */
    @Value("${application.cookie.cipherKey}")
    private String cookieCipherKey;

    @Value("${application.cookie.maxAge}")
    private int cookieMaxAge;

	@Value("${spring.redis.host}")
	private String host;

	@Value("${spring.redis.port}")
	private int port;

	@Bean
	public SimpleCookie rememberMeCookie() {
		// 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
		SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
		// <!-- 记住我cookie生效时间30天 ,单位秒;-->
		simpleCookie.setMaxAge(cookieMaxAge);
		return simpleCookie;
	}

	@Bean
	public CookieRememberMeManager rememberMeManager() {
		CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
		cookieRememberMeManager.setCookie(rememberMeCookie());
		cookieRememberMeManager.setCipherKey(Base64.decode(cookieCipherKey));
		return cookieRememberMeManager;
	}

	/**
	 * ShiroFilterFactoryBean Shiro过滤器,针对IP地址进行拦截是否需要对应权限
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilterFactoryBean() {
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager());
		Map<String, Filter> filters = new HashMap<>();
		//设置自定义Filter 返回json数据不跳转页面
		filters.put("authc",new MyFormAuthenticationFilter());
		shiroFilterFactoryBean.setFilters(filters);

		// 设置需要拦截的路径
		Map<String, String> filterChain = new HashMap<>();
		// 设置登出拦截
		filterChain.put("/logout", "anon");
		filterChain.put("/uploadFile", "anon");

		filterChain.put("/login", "anon");
		filterChain.put("/file", "anon");
		//后台管理自定义过滤器配置,验证是否是对应角色
		filterChain.put("/system/**","authc,roles[admin]");
		//门户网站自定义过滤器配置,验证是否是对应角色
//		filterChain.put("/portal/**","portalFilter");
		//APP管理自定义过滤器配置,验证是否是对应角色
//		filterChain.put("/app/**","appFilter");

		filterChain.put("/common/*", "anon");
		filterChain.put("/images/kaptcha.jpg", "anon");
		filterChain.put("/**/*.js", "anon");
		filterChain.put("/**/*.html", "anon");
		filterChain.put("/", "anon");

		// 拦截所有方法
		filterChain.put("/**", "authc");
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChain);

		// 设置拦截返回跳转的路径
		// 未登录跳转页面
		shiroFilterFactoryBean.setLoginUrl("/unlogin");
		//登录成功跳转页面
		// shiroFilterFactoryBean.setSuccessUrl("/");
		// 未授权跳转页面
		shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
		return shiroFilterFactoryBean;
	}

	/**
	 * DefaultWebSecurityManager 默认web安全管理器
	 */
	@Bean
	public DefaultWebSecurityManager defaultWebSecurityManager() {
		DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
		// 关联ream
		defaultWebSecurityManager.setRealm(authorizingRealm());
		//设置session manage
		defaultWebSecurityManager.setSessionManager(sessionManager());
		defaultWebSecurityManager.setCacheManager(cacheManager());
		defaultWebSecurityManager.setRememberMeManager(rememberMeManager());


		return defaultWebSecurityManager;
	}

	@Bean
	public DefaultWebSessionManager sessionManager() {
		SessionManager sessionManager = new SessionManager();
		sessionManager.setSessionDAO(redisSessionDAO());
		return sessionManager;
	}

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

	public RedisManager redisManager() {
		RedisManager redisManager = new RedisManager();
		redisManager.setHost(host);
		redisManager.setPort(port);
		return redisManager;
	}

	@Bean
	public RedisCacheManager cacheManager() {
		RedisCacheManager redisCacheManager = new RedisCacheManager();
		redisCacheManager.setRedisManager(redisManager());
		return redisCacheManager;
	}
	@Bean
	public AuthorizingRealm authorizingRealm() {
		UserRealm userRealm = new UserRealm();
		userRealm.setCredentialsMatcher((token, info) -> {
			LoginAuthToken userToken = (LoginAuthToken) token;
			// 要验证的明文密码
			String plaintext = new String(userToken.getPassword());
			// 数据库中的加密后的密文
			String hashed = info.getCredentials().toString();
			return BCrypt.checkpw(plaintext, hashed);
		});

		return userRealm;
	}
	/**
	 * 	AOP注入回调授权方法
	 * @return
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
	    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
	    authorizationAttributeSourceAdvisor.setSecurityManager(defaultWebSecurityManager());
	    return authorizationAttributeSourceAdvisor;
	}
}

4、HTML模拟跨域请求代码

<html>
	<head>
		<title>测试</title>
		<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
	</head>
	<body>
		<div>
			<button id="login">登录</button>
			<button id="logout">登出</button>
			
			
			<button id="get">获取</button>
			<span id="token"></span>
			
		</div>
		
		<script>
			$(function(){
				$("#login").click(function(){
					$.ajax({
						url:'http://localhost:8080/login',
						data:{userName:'zhangsan',password:'123456'},
						success:function(data){
							//登陆成功后 将token存储到浏览器
							$("#token").html(data.data)
						}
					})
				})
				$("#logout").click(function(){
					$.ajax({
						url:'http://localhost:8080/logout',
						//将登陆后返回的token返回给后台,设置请求头和请求参数都可以
						data:{token:$("#token").html()},
						success:function(data){
							alert(data.code+"-----"+data.msg);
						}
					})
				})
				
				
				$("#get").click(function(){
					$.ajax({
						url:'http://localhost:8080/common/quartz/list',
						data:{token:$("#token").html()},
						success:function(data){
							alert(data.code+"-----"+data.msg);
						}
					})
				})
			})
		</script>
	</body>
</html>

5、自定义验证shiro 验证过滤器解决未登录后重定向跳转问题

当用户未登录的情况下请求操作,默认shiro会重定向,前后端分离模式需要返回json对象,需要重写FormAuthenticationFilter 过滤器的onAccessDenied方法

package com.pengheng.config.shiro.filter;

import com.pengheng.model.ResultVo;
import com.pengheng.model.ResultVoFailure;
import com.pengheng.util.Toolkits;
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;
@Slf4j
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                if (log.isTraceEnabled()) {
                    log.trace("Login submission detected.  Attempting to execute login.");
                }
                return executeLogin(request, response);
            } else {
                if (log.isTraceEnabled()) {
                    log.trace("Login page view.");
                }
                //allow them to see the login page ;)
                return true;
            }
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Attempting to access a path which requires authentication.  Forwarding to the " +
                        "Authentication url [" + getLoginUrl() + "]");
            }
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setStatus(200);
            httpServletResponse.setContentType("application/json;charset=utf-8");
            //设置跨域允许,不然会提示跨域问题
            httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
            PrintWriter out = httpServletResponse.getWriter();
            ResultVo resultVo = new ResultVoFailure("用户未登录");
            out.println(Toolkits.toJson(resultVo));
            out.flush();
            out.close();
            return false;
        }
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值