SpringBoot集成Shiro

SpringBoot集成Shiro实现登录认证和权限管理

一、Shiro集成

依赖包(pom.xml):

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-core</artifactId>
	<version>1.2.2</version>
</dependency>

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

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-ehcache</artifactId>
	<version>1.2.2</version>
</dependency>

配置文件:

ehcache-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="shiroCache">

    <defaultCache
	    maxElementsInMemory="10000"
	    eternal="false"
	    timeToIdleSeconds="120"
	    timeToLiveSeconds="120"
	    overflowToDisk="false"
	    diskPersistent="false"
	    diskExpiryThreadIntervalSeconds="120"
    	/>
    	
    <!-- 登录记录缓存锁定10分钟 -->  
    <cache 
    	name="passwordRetryCache"  
        maxEntriesLocalHeap="2000"  
        eternal="false"  
        timeToIdleSeconds="3600"  
        timeToLiveSeconds="0"  
        overflowToDisk="false"  
        statistics="true">  
    </cache>  
            
</ehcache>

配置类:

ShiroConfiguration.java

package com.info.config;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.session.mgt.SessionManager;
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.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

/**
 * Shiro 配置
 */
@Configuration
public class ShiroConfiguration implements EnvironmentAware {
	
	@Autowired
	private Environment env;
	
	// implements EnvironmentAware:解决加上LifecycleBeanPostProcessor方法后Environment为空问题
	@Override
	public void setEnvironment(Environment environment) {
		this.env = environment;
	}

	/**
	 * 实例化SecurityManager,该类是shiro的核心类
	 */
    @Bean(name = "securityManager")
	public DefaultWebSecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setRealm(myShiroRealm());
		// 用户授权/认证信息Cache, 采用EhCache 缓存
		securityManager.setCacheManager(getEhCacheManager());
		// 自定义session管理,前后端分离后不能从cookie中取数据
		// securityManager.setSessionManager(sessionManager());
		return securityManager;
	}
    
	/**
	 * 配置缓存
	 */
	@Bean
	public EhCacheManager getEhCacheManager() {
		EhCacheManager em = new EhCacheManager();
		em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
		return em;
	}
	
	//自定义sessionManager
    @Bean
    public SessionManager sessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        // mySessionManager.setSessionDAO(redisSessionDAO());
        return mySessionManager;
    }


	/**
	 * 配置Realm
	 */
	@Bean(name = "myShiroRealm")
	public MyShiroRealm myShiroRealm() {
		MyShiroRealm realm = new MyShiroRealm();
		return realm;
	}  

	/**
	 * 注册DelegatingFilterProxy(Shiro) 集成Shiro有2种方法: 
	 * 1. 按这个方法自己组装一个FilterRegistrationBean(这种方法更为灵活,可以自己定义UrlPattern,
	 * 在项目使用中你可能会因为一些很但疼的问题最后采用它, 想使用它你可能需要看官网或者已经很了解Shiro的处理原理了)
	 * 2. 直接使用ShiroFilterFactoryBean(这种方法比较简单,其内部对ShiroFilter做了组装工作,无法自己定义UrlPattern,默认拦截 /*)
	 */
//	@SuppressWarnings({ "rawtypes", "unchecked" })
//	@Bean
//	public FilterRegistrationBean filterRegistrationBean() {
//		FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
//		filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
//		// 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
//		filterRegistration.addInitParameter("targetFilterLifecycle", "true");
//		filterRegistration.setEnabled(true);
//		filterRegistration.addUrlPatterns("/*");// 可以自己灵活的定义很多,避免一些根本不需要被Shiro处理的请求被包含进来
//		return filterRegistration;
//	}

	/**
	 * 该类可以保证实现了org.apache.shiro.util.Initializable 接口的shiro对象的init或者是destory方法被自动调用,
	 * 而不用手动指定init-method或者是destory-method方法 注意:如果使用了该类,则不需要手动指定初始化方法和销毁方法,否则会出错
	 * 加上后 Environment 为 NUll
	 */
	@Bean(name = "lifecycleBeanPostProcessor")
	public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
		return new LifecycleBeanPostProcessor();
	}

	/**
	 * 下面两个配置主要用来开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持
	 */
	@Bean
	public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
		DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
		daap.setProxyTargetClass(true);
		return daap;
	}

	@Bean
	public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
		AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
		aasa.setSecurityManager(securityManager);
		return aasa;
	}

	@Bean(name = "shiroFilter")
	public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		// 必须设置 SecurityManager
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
		shiroFilterFactoryBean.setLoginUrl("/login");
		// 登录成功后要跳转的连接
		// shiroFilterFactoryBean.setSuccessUrl("/index");
		// 设置无权限访问页面
		shiroFilterFactoryBean.setUnauthorizedUrl("/403");

		loadShiroFilterChain(shiroFilterFactoryBean);
		return shiroFilterFactoryBean;
	}
	
	/**
	 * 加载shiroFilter权限控制规则(从数据库读取然后配置)
	 */
	private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
		// 下面这些规则配置最好配置到配置文件中
		Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
		
		try {
			List<String> anons = Arrays.asList(env.getProperty("anon").split(","));
			System.out.println(anons.toString());
		} catch (NullPointerException e) {
		}

		// 2.不拦截的请求
		filterChainDefinitionMap.put("/css/**", "anon");
		filterChainDefinitionMap.put("/js/**", "anon");
		filterChainDefinitionMap.put("/login", "anon");
		// 此处将logout页面设置为anon,而不是logout,因为logout被单点处理,而不需要再被shiro的logoutFilter进行拦截
		filterChainDefinitionMap.put("/logout", "anon");
		filterChainDefinitionMap.put("/error", "anon");
		// 3.拦截的请求(从本地数据库获取或者从casserver获取(webservice,http等远程方式),看你的角色权限配置在哪里)
		// 需要有add权限才能访问
		// filterChainDefinitionMap.put("/index", "perms[add]"); 
		// 需要是admin角色才能访问
		filterChainDefinitionMap.put("/index", "roles[admin]");

		// 4.登录过的不拦截
		filterChainDefinitionMap.put("/**", "authc");

		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
	}

}

MyShiroRealm.java

package com.info.config;

import java.util.List;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
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 org.springframework.beans.factory.annotation.Autowired;

import com.info.dao.UserDao;
import com.info.entity.Role;
import com.info.entity.User;

public class MyShiroRealm extends AuthorizingRealm {

    private static final Logger LOG = LoggerFactory.getLogger(MyShiroRealm.class);

    @Autowired
    private UserDao userDao; 

	/**
	 * 此方法调用 hasRole, hasPermission 的时候才会进行回调
	 * 
	 * 权限信息(授权): 1、如果用户正常退出,缓存自动清空; 2、如果用户非正常退出,缓存自动清空;
	 * 3、如果我们修改了用户的权限,而用户不退出系统,修改的权限无法立即生效。 (需要手动编程进行实现;放在service进行调用)
	 * 在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例, 调用clearCached方法;
	 * Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
	 * 
	 * 权限认证,为当前登录的Subject授予角色和权限 经测试:本例中该方法的调用时机为需授权资源被访问时
	 * 经测试:并且每次访问需授权资源时都会执行该方法中的逻辑,这表明本例中默认并未启用AuthorizationCache
	 * 经测试:如果连续访问同一个URL(比如刷新),该方法不会被重复调用,Shiro有一个时间间隔(也就是cache时间,在ehcache-shiro.xml中配置),超过这个时间间隔再刷新页面,该方法会被执行
	 */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        LOG.info("################## 执行Shiro权限认证 ##################");
		// 获取当前登录输入的用户名,等价于(String) principalCollection.fromRealm(getName()).iterator().next();
		String loginName = (String) super.getAvailablePrincipal(principalCollection);
		// 到数据库查是否有此对象
		// 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        User user=userDao.findByCode(loginName);
        
		if (user != null) {
			// 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
			SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
			// 用户的角色集合
			info.setRoles(user.getRolesName());
			// 用户的角色对应的所有权限,如果只使用角色定义访问权限,下面的四行可以不要
//			List<Role> roleList = user.getRoleList();
//			for (Role role : roleList) {
//				info.addStringPermissions(role.getPermissionsName());
//			}
			// 或者按下面这样添加添加一个角色, 不是配置意义上的添加, 而是证明该用户拥有admin角色
			// simpleAuthorInfo.addRole("admin");
			// 添加权限
			// simpleAuthorInfo.addStringPermission("admin:manage");
			return info;
		}
		// 返回null的话,就会导致任何用户访问被拦截的请求时,都会自动跳转到 unauthorizedUrl 指定的地址
		return null;
    }

	/**
	 * 登录认证
	 * 判断当前登录用户是否正确,如果使用CAS来实现单点登录就不需要在写
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		// UsernamePasswordToken:对象用来存放提交的登录信息
		UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
		LOG.info("验证当前Subject时获取到token为:" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));

		// 判断用户是否存在
		User user = userDao.findByCode(token.getUsername());
		if (user != null) {
			// 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验
			return new SimpleAuthenticationInfo(user.getCode(), user.getPassword(), getName());
		}
		return null;
	}
}

MySessionManager.java 前后端分离框架用到

package com.info.config;

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;

/**
 * 自定义sessionId获取
 * 传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,在前后端分离的项目中(也可在移动APP项目使用),
 * 我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。
 * 自定义MySessionManager类继承DefaultWebSessionManager类,重写getSessionId方法,代码如下
 */
public class MySessionManager extends DefaultWebSessionManager {

	private static final String AUTHORIZATION = "Authorization";

	private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

	public MySessionManager() {
		super();
	}

	@Override
	protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
		String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
		System.out.println("id = " + id);
		// 如果请求头中有 Authorization 则其值为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);
		}
	}
}

二、代码实现

DAO:

package com.info.dao;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import com.info.entity.User;

@Repository
public interface UserDao extends CrudRepository<User, Long> {

	User findByCode(String code);
	
}

Controller:

package com.info.controller;

import java.util.Map;

import javax.validation.Valid;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.info.entity.User;

@Controller
public class ShiroController {

    private static final Logger LOG = LoggerFactory.getLogger(ShiroController.class);

	@RequestMapping(value = "/login", method = RequestMethod.GET)
	public String loginForm(Model model) {
		return "login";
	}

	// 登录验证
	@RequestMapping(value = "/login", method = RequestMethod.POST)
	public String login(@Valid User user, BindingResult bindingResult, RedirectAttributes redirectAttributes) {

		if (bindingResult.hasErrors()) {
			return "login";
		}

		UsernamePasswordToken token = new UsernamePasswordToken(user.getCode(), user.getPassword());
		// 获取当前的 Subject
		Subject subject = SecurityUtils.getSubject();
		try {
			// 在调用了login 方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查
			// 每个Realm都能在必要时对提交的AuthenticationTokens作出反应
			// 所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法
			LOG.info("进行登录验证 ===> 验证开始");
			subject.login(token);
			LOG.info("进行登录验证 ===> 验证通过");
		} catch (UnknownAccountException uae) {
			LOG.info("进行登录验证 ===> 验证未通过,未知账户");
			redirectAttributes.addFlashAttribute("message", "未知账户");
		} catch (IncorrectCredentialsException ice) {
			LOG.info("进行登录验证 ===> 验证未通过,错误的凭证");
			redirectAttributes.addFlashAttribute("message", "密码不正确");
		} catch (LockedAccountException lae) {
			LOG.info("进行登录验证 ===> 验证未通过,账户已锁定");
			redirectAttributes.addFlashAttribute("message", "账户已锁定");
		} catch (ExcessiveAttemptsException eae) {
			LOG.info("进行登录验证 ===> 验证未通过,错误次数过多");
			redirectAttributes.addFlashAttribute("message", "用户名或密码错误次数过多");
		} catch (AuthenticationException ae) {
			// 通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景
			LOG.info("进行登录验证 ===> 验证未通过,堆栈轨迹如下");
			ae.printStackTrace();
			redirectAttributes.addFlashAttribute("message", "用户名或密码不正确");
		}
		// 验证是否登录成功
		if (subject.isAuthenticated()) {
			LOG.info("进行登录验证 ===> 登录认证通过(这里可以进行一些认证通过后的一些系统参数初始化操作)");
			return "redirect:/index";
		} else {
			token.clear();
			return "redirect:/login";
		}
	}   
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值