欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SpringBoot 整合Shiro 实现前后端分离

程序员文章站 2022-03-15 11:51:41
...


  最近着手开发一个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;
        }
    }
}