目录
Shiro是一个对用户登录与访问权限进行校验的框架,可以方便地与Spring进行集成。本文讲解如何使用Shiro进行登录和权限校验,因为项目中需要获取所有活跃的session,使用了SessionDAO来存储session。
maven依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.13.0</version>
</dependency>
<!-- shiro 整合sping -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.13.0</version>
</dependency>
代码配置
AuthorizingRealm
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.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Value;
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.digitelectric.network_manage.entity.po.User;
import com.digitelectric.network_manage.mapper.UserMapper;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomRealm extends AuthorizingRealm {
@Resource
UserMapper userMapper;
@Resource
ValueOperations<String, String> valueOperations;
@Resource
RedisTemplate redisTemplate;
@Resource
SessionDAO sessionDAO;
@Value("${shiro.session-timeout}")
private Long timeout;
//获取当前用户的权限
@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()));
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("name", name));
if (user==null || !password.equals(user.getPassword())) {
throw new AuthenticationException("用户名或密码错误");
}
Session session = SecurityUtils.getSubject().getSession();
String token = (String)SecurityUtils.getSubject().getSession().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);
}
}
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.getName());
session.setAttribute(WebConstants.LOGIN_USER_TOKEN, token);
session.setAttribute(WebConstants.LOGIN_USER, user);
session.setTimeout(timeout);
} else {
log.warn("token不为空");
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token, password, getName());
return simpleAuthenticationInfo;
}
}
FormAuthenticationFilter,校验过滤器,在session过期之后,通过请求头中的token来获取登录信息。
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.session.Session;
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.digitelectric.network_manage.entity.po.User;
import com.digitelectric.network_manage.entity.vo.R;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MyAuthenticationFilter extends FormAuthenticationFilter {
public MyAuthenticationFilter(Long timeout, ValueOperations<String, Object> valueOperations) {
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 = this.getSubject(httpServletRequest, httpServletResponse).getSession();
User user = (User)session.getAttribute(WebConstants.LOGIN_USER);
String token = httpServletRequest.getHeader("token");
if (user==null) {
log.debug("没有获取到登录信息,当前token值:{}", token);
//通过头部的token,从缓存中获取用户,并写入到session中
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.getName());
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.getName());
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();
}
}
WebConstants
public interface WebConstants {
String LOGIN_USER_ID = "loginUserId", LOGIN_USER_TOKEN = "loginUserToken", LOGIN_USER_NAME = "loginUserName", LOGIN_USER = "loginUser";
}
Shiro相关Bean注入配置类
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.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
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.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.redis.core.ValueOperations;
@Configuration
public class ShiroConfig {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, @Value("${shiro.session-timeout}") Long sessionTimeout, ValueOperations<String, Object> valueOperations) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login", "anon");
//swagger相关的请求,放行
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/ws/**", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
Map<String, Filter> filters = new HashMap<>();
filters.put("authc", new MyAuthenticationFilter(sessionTimeout, valueOperations));
shiroFilterFactoryBean.setFilters(filters);
return shiroFilterFactoryBean;
}
@Bean
public SessionDAO sessionDAO() {
return new MemorySessionDAO();
}
/**
* 设置sessionDAO
* @param sessionDAO
* @return
*/
@Bean
public SecurityManager securityManager(SessionDAO sessionDAO) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(sessionDAO);
sessionManager.setGlobalSessionTimeout(2 * 3600000);
securityManager.setSessionManager(sessionManager);
securityManager.setRealm(customRealm());
return securityManager;
}
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
@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;
}
}
application.yml配置
shiro:
session-timeout: 999999
登录/退出/权限验证
登录
@PostMapping("login")
@ApiOperation(value = "登录")
public R<User> login(String name, String password) {
Subject subject = SecurityUtils.getSubject();
User user = userMapper.selectOne(new QueryWrapper<User>().eq("name", name));
if (user==null) {
throw new AuthenticationException("用户名或密码错误");
}
UsernamePasswordToken token = new UsernamePasswordToken(name, MD5SecurityUtil.encryptMD5(password));
subject.login(token); // 登录动作
return R.ok(getCurrentLoginUser());
}
public User getCurrentLoginUser() {
User user = (User)
SecurityUtils.getSubject().getSession().getAttribute(WebConstants.LOGIN_USER);
if(user==null) {
throw new AccountException();
}
return user;
}
退出
@PostMapping("logout")
@ApiOperation(value = "登出")
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);
redisTemplate.delete(token);
subject.getSession().removeAttribute(WebConstants.LOGIN_USER_NAME);
subject.logout();
return R.ok();
}
权限校验,注意logical,表示校验权限时是包含所有权限,还是包含其中一个权限即可。
@RequiresRoles(value = {"ADMIN", "NORMAL"}, logical = Logical.OR)
@ApiOperation("添加")
@PostMapping
public R add(@RequestBody District district) {
if (districtMapper.selectCount(new QueryWrapper<District>().eq("name", district.getName())) > 0) {
return R.fail("区域名重复");
}
districtMapper.insert(district);
return R.ok(district);
}