目录

SpringBoot请求权限控制Shiro

SpringBoot请求权限控制——Shiro

Shiro是一个请求权限校验框架,本文介绍如何与SpringBoot集成使用。

登录校验

1、增加maven依赖

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

2、shiro配置类

package com.wzu.utilities.config.shiro;

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

import javax.servlet.Filter;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.ValueOperations;

@Configuration
public class ShiroConfig {

	@Bean(name = "shiroFilter")
	public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, ValueOperations<String, Object> valueOperations, @Value("${shiro.session-timeout}") Long sessionTimeout) {
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		
		LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
		filterChainDefinitionMap.put("/login", "anon");
        
        filterChainDefinitionMap.put("/**", "authc");
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
		
        Map<String, Filter> filters = new HashMap<>();
        filters.put("authc", new MyAuthenticationFilter(valueOperations, sessionTimeout));
        shiroFilterFactoryBean.setFilters(filters);
		return shiroFilterFactoryBean;
	}
	
	@Bean
	public SecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setRealm(customRealm());
		return securityManager;
	}
	
	@Bean
	public CustomRealm customRealm() {
		return new CustomRealm();
	}
}

授权逻辑类CustomRealm

package com.wzu.utilities.config.shiro;

import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
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.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.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.wzu.utilities.entity.po.User;
import com.wzu.utilities.mapper.UserMapper;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CustomRealm extends AuthorizingRealm {
	
	@Resource
	UserMapper userMapper;
	
	@Resource
	ValueOperations<String, String> valueOperations;
	
	@Resource
	RedisTemplate redisTemplate;

	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		String token = (String) principals.getPrimaryPrincipal();
		String userStr = valueOperations.get(token);
		if (StringUtils.isBlank(userStr)) {
			throw new AccountException("登录失效,请重新登录");
		}
		
		User user = JSONObject.parseObject(userStr, User.class);
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		//TODO:info.setRoles();
		return info;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		String name = (String) authenticationToken.getPrincipal();
		String password = new String((char[]) authenticationToken.getCredentials());
		
		User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", name));
		if (user==null || !password.equals(user.getPassword())) {
			throw new AuthenticationException("用户名或密码错误");
		}

		Session session = SecurityUtils.getSubject().getSession();
		String token = (String)session.getAttribute(WebConstants.LOGIN_USER_TOKEN);
		if (StringUtils.isBlank(token)) {
//			for(Session activeSession:sessionDAO.getActiveSessions()) {
//				if(user.getId().equals(activeSession.getAttribute(WebConstants.LOGIN_USER_ID)) && !activeSession.getId().equals(session.getId())) {
//					String tmpToken = (String)activeSession.getAttribute(WebConstants.LOGIN_USER_TOKEN);
//					if(StringUtils.isNotBlank(tmpToken)) {
//						redisTemplate.delete(tmpToken);
//					}
//					sessionDAO.delete(activeSession);
//				}
//			}
			
//			if (user.getLoginNum()!=1) {
//				user.setLoginNum(user.getLoginNum()+1);
//				User toUpdate = new User();
//				toUpdate.setId(user.getId());
//				toUpdate.setLoginNum(user.getLoginNum());
//				userMapper.updateById(toUpdate);
//			}
			
			token = System.currentTimeMillis() + "-" + user.getId();
			user.setToken(token);
			
			valueOperations.set(token, JSON.toJSONString(user), 1, TimeUnit.DAYS);
			session.setAttribute(WebConstants.LOGIN_USER_ID, user.getId());
	    	session.setAttribute(WebConstants.LOGIN_USER_NAME, user.getRealname());
	    	session.setAttribute(WebConstants.LOGIN_USER_TOKEN, token);
	    	session.setAttribute(WebConstants.LOGIN_USER, user);
	    	
		} else {
			log.warn("token不为空");
		}
		
		SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token, password, getName());
		return simpleAuthenticationInfo;
	}

}

授权验证不通过时的filter类 MyAuthenticationFilter

package com.wzu.utilities.config.shiro;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

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

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.data.redis.core.ValueOperations;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.wzu.utilities.entity.po.User;
import com.wzu.utilities.entity.vo.R;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MyAuthenticationFilter extends FormAuthenticationFilter {
	
	public MyAuthenticationFilter(ValueOperations<String, Object> valueOperations, Long timeout) {
		this.timeout = timeout;
		this.valueOperations = valueOperations;
	}

	private ValueOperations<String, Object> valueOperations;
	
	private Long timeout;

	@Override
	protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
		HttpServletRequest httpServletRequest = (HttpServletRequest) request;
		HttpServletResponse httpServletResponse = (HttpServletResponse) response;
		httpServletResponse.setStatus(200);
		httpServletResponse.setContentType("application/json;charset=utf8");
		Session session = SecurityUtils.getSubject().getSession();
        User user = (User)session.getAttribute(WebConstants.LOGIN_USER);
        String token = httpServletRequest.getHeader("token");
        if (user==null) {
        	log.debug("没有获取到登录信息,当前token值:{}", token);

        	//如果session失效,通过header中的token来获取授权信息
            if(StringUtils.isNotBlank(token)) {
            	String userStr = (String)valueOperations.get(token);
            	if (StringUtils.isBlank(userStr)) {
            		writeNeedLoginInfo(httpServletResponse);
            		return false;
            	}
            	user = JSONObject.parseObject(userStr, User.class);
            	session.setAttribute(WebConstants.LOGIN_USER_ID, user.getId());
            	session.setAttribute(WebConstants.LOGIN_USER_NAME, user.getRealname());
            	session.setAttribute(WebConstants.LOGIN_USER_TOKEN, token);
            	session.setAttribute(WebConstants.LOGIN_USER, user);
            	session.setTimeout(timeout);
            	log.debug("没有获取到登录信息2,当前token值:{}", token);
                return true;
            }
        } else {
        	log.debug("获取到登录信息,当前token值:{}", token);
        	session.setAttribute(WebConstants.LOGIN_USER_ID, user.getId());
        	session.setAttribute(WebConstants.LOGIN_USER_NAME, user.getRealname());
        	session.setAttribute(WebConstants.LOGIN_USER_TOKEN, token);
        	session.setAttribute(WebConstants.LOGIN_USER, user);
        	session.setTimeout(timeout);
            return true;
        }
        
        writeNeedLoginInfo(httpServletResponse);
        return false;
    }
	
	/**
	 * 返回自定义需要登录的json
	 * @param httpServletResponse
	 * @throws IOException
	 */
	private void writeNeedLoginInfo(HttpServletResponse httpServletResponse) throws IOException {
        PrintWriter out = httpServletResponse.getWriter();
        out.println(JSON.toJSONString(R.failWithCode(7777, "请先登录")));
        out.flush();
        out.close();
    }
	
	
}

登录、登出、登录信息获取的请求代码

    @PostMapping("login")
	public R<User> login(String username, String password, HttpServletRequest request, HttpServletResponse response) {
		Subject subject = SecurityUtils.getSubject();
		User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
		if (user==null) {
			throw new AuthenticationException("用户名或密码错误");
		}
		UsernamePasswordToken token = new UsernamePasswordToken(username, MD5SecurityUtil.encryptMD5(password));
		subject.login(token);
		return R.ok(getCurrentLoginUser());
	}


    @PostMapping("logout")
    public R<User> logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.getSession().removeAttribute(WebConstants.LOGIN_USER);
        subject.getSession().removeAttribute(WebConstants.LOGIN_USER_ID);
        String token = (String)subject.getSession().removeAttribute(WebConstants.LOGIN_USER_TOKEN);
        if(StringUtils.isNotBlank(token)) {
        	redisTemplate.delete(token);
        }
        subject.getSession().removeAttribute(WebConstants.LOGIN_USER_NAME);
        
        subject.logout();
        return R.ok();
    }

    @GetMapping("info")
	public R<User> getUserInfo() {
		User user = getCurrentLoginUser();;
		return R.ok(user);
	}

    public User getCurrentLoginUser() {
		 User user = (User) SecurityUtils.getSubject().getSession().getAttribute(WebConstants.LOGIN_USER);
		 if(user==null) {
			 throw new AccountException();
		 }
		 return user;
	 }

对登录失败时的AccountException进行拦截,返回需要登录的json字符串:

package com.wzu.utilities.web.aspect;


import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import com.wzu.utilities.entity.enums.ControllerResultEnum;
import com.wzu.utilities.entity.vo.R;

import lombok.extern.slf4j.Slf4j;

/**
 * @Author zhangyugu
 * @Date 2021/4/24 7:30 下午
 * @Version 1.0
 */
@Slf4j
@ControllerAdvice
public class WebExceptionHandler {

    @ResponseBody
    @ExceptionHandler
    public R exceptionHandler(Exception e) {
        log.error(e.getMessage(), e);
        if(e instanceof AccountException) {
            return R.fail(e.getMessage());
        }
        if(e instanceof UnauthorizedException) {
            return R.failWithCode(ControllerResultEnum.USER_HASNO_AUTH.getCode(), e.getMessage());
        }
        if(e instanceof AuthenticationException) {
            return R.failWithCode(ControllerResultEnum.NEED_LOGIN.getCode(), e.getMessage());
        }
        if(e instanceof UnauthenticatedException) {
            return R.failWithCode(ControllerResultEnum.NEED_LOGIN.getCode(), e.getMessage());
        }
        return R.fail(e.getMessage());
    }
}
package com.wzu.utilities.entity.enums;

/**
 * @Author zhangyugu
 * @Date 2021/4/8 11:47 上午
 * @Version 1.0
 */
public enum ControllerResultEnum {

    ERROR(300, "运行出错"),
    REQUEST_TIMEOUT(408, "请求超时"),
    SERVER_DOWN(503,"服务器超负载,或者停机维护"),

    NEED_LOGIN(7777, "需要登录"),
    USER_HASNO_AUTH(1003, "用户权限不足"),
    VIOLATION_EXCEPTION(1200, "验证数据出现错误!"),

    RESULT_NULL(1300,"结果为空!"),

    FILE_UPLOAD_PATH_NOT_EXIST(1503,"文件路径不存在");


    Integer code;

    String message;

    ControllerResultEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

请求增加角色/权限验证

ShiroConfig配置类增加如下Bean

 @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 以下两个bean(其中DefaultAdvisorAutoProxyCreator可选)
     * 用于启用 @RequiresRoles 和 @RequiresPermissions 注解
     * @return
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SessionDAO sessionDAO) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager(sessionDAO));
        return authorizationAttributeSourceAdvisor;
    }

在CustomRealm中增加角色、权限的设置

@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		String token = (String) principals.getPrimaryPrincipal();
		String userStr = valueOperations.get(token);
		if (StringUtils.isBlank(userStr)) {
			throw new AccountException("登录失效,请重新登录");
		}
		
		User user = JSONObject.parseObject(userStr, User.class);
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		info.setRoles(Sets.newHashSet(user.getRole()));
		//TODO:权限码 info.setStringPermissions();
		return info;
	}

在Controller类或其方法上增加注解

@RequiresRoles("ADMIN") //验证角色
@RequiresRoles(value= {"ADMIN", "SUPER_ADMIN"}, logical=Logical.OR) //验证角色,ADMIN或者SUPER_ADMIN,注意需要用"或"
@RequiresPermissions("ADD_USER") //验证权限
@RequiresPermissions(value= {"ADD_USER", "DELETE_USER"}, logical=Logical.OR) //验证权限, ADD_USER  DELETE_USER