Springboot整合Shiro实现登录认证

一、概述
Shiro 是一个功能强大且易于使用的轻量级Java安全框架,包括身份验证、授权、加密及会话管理,使用Shiro易于理解的API,可以轻松地保护任何应用程序。
二、Shiro主要组成
1.首先主要包括三大实体:Subject、Realm、和SecurityManager
Subject即当前用户,我们可以通过Subject自带的 。SecurityUtils.getSubject()方法获取当前对象,并通过当前用户拿到shiro的session,进行后续的认证授权等。

SecurityManager即安全管理器,可以视为拦截器,拦截请求,并转至shiro中处理。

Realm,可以有1个或多个Realm,即安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;一般由用户自定义实现。

三、Springboot如何使用Shiro实现登录认证
1.添加依赖


    org.apache.shiro
    shiro-spring
    1.4.0

2.定义Shiro配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

import com.suntree.entity.SysUser;
import com.suntree.service.UserService;
import com.suntree.utils.YAMLUtils;
import com.suntree.entity.Power;
import com.suntree.utils.CommonUtil;

import com.suntree.entity.Role;

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

import javax.servlet.Filter;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
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.subject.PrincipalCollection;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
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.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
@Configuration
public class ShiroConfig {
	@Autowired
    private YAMLUtils yaml;
	private static Log logger = LogFactory.getLog(ShiroConfig.class);

	@Bean
	public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) throws Exception {
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
		Map filters = new LinkedHashMap();
		shiroFilterFactoryBean.setFilters(filters);
		//权限控制map
        HashMap filterMap = new LinkedHashMap<>();
        filterMap.put("/api/city/page", "anon");
        filterMap.put("/api/topic/", "anon");
        filterMap.put("/api/**", "authc");
       // filterMap.put(yaml.logout_url, "logout");
        // 登录 url
        shiroFilterFactoryBean.setLoginUrl(yaml.login_url);
        // 未授权 url
        shiroFilterFactoryBean.setUnauthorizedUrl(yaml.unauthorized_url);
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
		return shiroFilterFactoryBean;
	}
	@Bean
	public SecurityManager securityManager(@Qualifier("myShiroRealm") MyShiroRealm myShiroRealm) {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setRealm(myShiroRealm);
		// 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
     // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
		return securityManager;
	}
    /**
     * 身份认证realm; (这个需要自己写,账号密码校验;权限等)
     *
     * @return
     */
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(credentialsMatcher());
        myShiroRealm.setCacheManager(cacheManager());//設置緩存
        return myShiroRealm;
    }

	/*
	    @Bean
	    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
	        return new LifecycleBeanPostProcessor();
	    }*/
	 
	/**
     * cacheManager 缓存 redis实现
     * 
     * @return
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }
    /**
     * 配置shiro redisManager
     * 
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(yaml.redis_host);
        redisManager.setPort(yaml.redis_port);
        redisManager.setExpire(yaml.redis_cache);// 配置过期时间
        redisManager.setTimeout(0);
        redisManager.setPassword(yaml.redis_passwd);
        logger.info("配置redis连接设置##########" + yaml.redis_host + ":::" + yaml.redis_port);
        return redisManager;
    }
    /**
     * Session Manager
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
       // Collection listeners = new ArrayList();
       // listeners.add(new BDSessionListener());
       // sessionManager.setSessionListeners(listeners);
        sessionManager.setSessionDAO(sessionDAO());
        return sessionManager;
    }
    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO sessionDAO() {
    	logger.info("是否使用redis缓存");
    	RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
    	redisSessionDAO.setRedisManager(redisManager());
    	logger.info("设置redisSessionDAO");
    	return redisSessionDAO;
    }
    /**
     * 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持; Controller才能使用@RequiresPermissions
     * 
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
       // log.info("authorizationAttributeSourceAdvisor()");
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    /**
     * AOP式方法级权限检查
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator creator=new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
    /**
     * @return 密码匹配器
     */
    @Bean
    public CredentialsMatcher credentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        return hashedCredentialsMatcher;
    }
    public class MyShiroRealm extends AuthorizingRealm {
    	
    	@Autowired
    	@Lazy
    	private UserService userService;
    	//private final Log log = LogFactory.getLog(MyShiroRealm.class);
    	  /**
         * 用户验证
         * @param token 账户数据
         * @return
         * @throws AuthenticationException 根据账户数据查询账户。根据账户状态抛出对应的异常
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        	//获取用户的输入的账号
        	String accNum = (String) token.getPrincipal();
        	//这里需注意。看别人的教程有人是这样写的String password = (String) token.getCredentials();
        	//项目运行的时候报错,发现密码不正确。后来进源码查看发现将密码注入后。Shiro会进行转义将字符串转换成字符数组。
        	//源码:this(username, password != null ? password.toCharArray() : null, false, null);
        	//不晓得是否是因为版本的原因,建议使用的时候下载源码进行查看
        	//  String password = new String((char[]) token.getCredentials());
        	//通过username从数据库中查找 User对象,如果找到,没找到.
        	//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        	SysUser user = userService.getUserByAcc(accNum);
        //	SysUser sysUser = userService.getUserByAcc(accNum);
        //	sysUser.setPasswd("");
        	if(user==null){
        		throw new UnknownAccountException();
        	} else if(yaml.is_lock.equals(user.getIsLock())) {
        		throw new LockedAccountException();
        	}else {
        		SimpleAuthenticationInfo authorizationInfo = new SimpleAuthenticationInfo(user,user.getPasswd(),this.getName());
        		return authorizationInfo;
        	}
        }
        /**
         * 配置权限 注入权限
         * @param principals
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
            System.out.println("--------权限配置-------");
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            SysUser user = (SysUser) principals.getPrimaryPrincipal();
            try {
            	user = userService.getUserDetailInfoByUserId(user.getUserID());
                //注入角色(查询所有的角色注入控制器)
            	List roleList = user.getRoleList();
                for (Role role: roleList){
                    authorizationInfo.addRole(role.getRoleName());
                    //注入角色所有权限(查询用户所有的权限注入控制器)
                    if(CommonUtil.check(role.getPowerList())) {
            			for (Power permission : role.getPowerList()) {
            				authorizationInfo.addStringPermission(permission.getUrl());
            			}
            		}
                }
            }catch (Exception e){
                e.printStackTrace();
                logger.error(e.getMessage());
            }
            return authorizationInfo;
        }
    }
}

此处进行登录验证时使用了MD5加密,即数据库中存储的密码应是MD5加密后的密文密码,而不是明文密码。

3.在Controller层编写登录接口

    import java.util.Date;

import org.apache.shiro.SecurityUtils;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.suntree.service.UserService;
import com.suntree.utils.CommonUtil;
import com.suntree.utils.DateUtil;
import com.suntree.utils.YAMLUtils;
import com.suntree.commons.ApiResult;
import com.suntree.entity.SysUser;
import com.suntree.entity.UserToken;
import com.suntree.mapper.UserTokenMapper;

@RestController
public class LoginController {
	@Autowired
    private UserService userService;
	@Autowired
	private UserTokenMapper userTokenMapper;
	@Autowired
	private YAMLUtils yaml;
	/**
	 * 登录
	 * @param user
	 * @return
	 */
	@PostMapping("/login")
    public ApiResult login(@RequestBody SysUser user) {
		ApiResult result = new ApiResult();
		UserToken userToken=userTokenMapper.getTokenByToken(user.getToken());
		String kaptcha=user.getKaptcha();
		if(!kaptcha.equalsIgnoreCase(userToken.getKaptcha())) {
			return result.failed("验证码错误");
		}
		if(DateUtil.checkBegTmAndEndTm(user.getExpireTm(),DateUtil.dateToYMDHMS(new Date()))) {
			return result.failed("验证码已过期");
		}
		Subject subject = SecurityUtils.getSubject();
		subject.getSession().setTimeout(yaml.session_timeout);
		UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getAccNum(), user.getPasswd());
		try {
			subject.login(usernamePasswordToken);
		} catch (UnknownAccountException uae) {
			return result.failed("账号或密码错误");
		} catch (IncorrectCredentialsException ice) {
			return result.failed("账号或密码错误");
		} catch (LockedAccountException lae) {
			return result.failed("账号被冻结");
		} catch (RuntimeException re) {
			return result.failed(re.getMessage());
		}
		SysUser loginUser=CommonUtil.getLoginUser();
		SysUser data = userService.getUserDetailInfoByUserId(loginUser.getUserID());
		return result.success(data, "登录成功");
	}
	/**
	 * 退出登录
	 * @return
	 */
	@GetMapping("/logout")
    public ApiResult logout() {
		ApiResult result = new ApiResult();
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        subject.getSession().setTimeout(1000);
        return result.success("退出登录成功", "退出登录成功");
	}

这样就实现了使用Shiro完成登录认证,大家可以自己尝试下。

另外,如果我们想要控制对于接口访问权限,也可以使用Shiro相关注解实现,比如像 @RequestPermissions这样的注解就可以进行控制用户的权限,这里就不做过多说明了。

你可能感兴趣的:(spring,boot,后端,java)