Shiro无状态登录介绍
使用Shiro实现有状态登录,即用户登录状态存储在服务器Session中,使用Shiro实现比较简单,我这里暂不进行讨论。
在一些环境中,可能需要把 Web 应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像Session这种东西,而是每次请求时带上token之类的进行用户判断。
使用Shiro实现无状态登录的主要步骤有,禁用缓存、设置不创建session、关闭Session验证、关闭Session存储、注入自定义拦截器、开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)等。
1.创建SpringBoot项目,并添加以下依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
2.添加登录接口
获取token,这里token写死为admin
@RestController
@RequestMapping("/auth")
public class LoginController {
@GetMapping("/token")
public Object token(String username, String password) {
//登录并返回token
String token = "admin";
return ResponseEntity.ok(token);
}
}
3.添加业务接口
添加增删改查接口,并设置需要的访问权限
@RestController
@RequestMapping("/service")
public class ServiceController {
@RequestMapping("/create")
@RequiresPermissions("service:create")
public ResponseEntity add() {
return ResponseEntity.ok("增加成功");
}
@RequestMapping("/delete")
@RequiresPermissions("service:delete")
public ResponseEntity delete() {
return ResponseEntity.ok("删除成功");
}
@RequestMapping("/update")
@RequiresPermissions("service:update")
public ResponseEntity update() {
return ResponseEntity.ok("修改成功");
}
@RequestMapping("/read")
@RequiresPermissions("service:read")
public ResponseEntity read() {
return ResponseEntity.ok("查询成功");
}
}
4.创建SubjectFactory
关键是设置不创建Session
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
public Subject createSubject(SubjectContext context) {
//不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
5.创建拦截器
主要功能就是拦截http请求,进行认证授权,注意不要拦截登录请求
import com.alibaba.fastjson.JSON;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.AccessControlFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Set;
public class StatelessAuthcFilter extends AccessControlFilter {
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
String uri = req.getRequestURI();
Set<String> passUrl = new HashSet<>();
//配置不需要认证即可访问的地址,如登录接口
passUrl.add("/auth/token");
if (passUrl.contains(uri)) {
//允许直接访问
return true;
}
//不允许访问,执行onAccessDenied的拦截内容
return false;
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
//获取请求头中的token值
String token = req.getHeader("token");
if (token == null) {
returnLoginError(response);
return false;
}
//将token传入AuthenticationToken
AuthenticationToken authenticationToken = new AuthenticationToken() {
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
};
try {
//委托给Realm进行认证
getSubject(request, response).login(authenticationToken);
} catch (Exception e) {
//登录失败
returnLoginError(response);
return false;
}
return true;
}
//登录失败时默认返回401状态码
private void returnLoginError(ServletResponse response) throws IOException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
ResponseEntity err = ResponseEntity.err(ResultCode.UN_AUTHORIZED.getValue(), ResultCode.UN_AUTHORIZED.getDescription());
writer.write(JSON.toJSONString(err));
}
}
6.创建StatelessRealm
在StatelessAuthcFilter拦截器中,会调用StatelessRealm进行token认证及用户授权。
我这里设置token为admin时拥有增删改查的权限。
token为user时,只拥有查的权限。
其他token,无任何权限。
import org.apache.shiro.authc.*;
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 java.util.ArrayList;
import java.util.List;
public class StatelessRealm extends AuthorizingRealm {
/**
* 设置支持的AuthenticationToken
*/
public boolean supports(AuthenticationToken token) {
return true;
}
/**
* 授权
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Long userId = (Long) principals.getPrimaryPrincipal();
//根据userId查询用户用户权限,并授权
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
List<String> permissions = new ArrayList();
if (userId == 0) {
//拥有所有权限
permissions.add("service:create");
permissions.add("service:delete");
permissions.add("service:update");
permissions.add("service:read");
} else if (userId == 1) {
//只有读的权限
permissions.add("service:read");
} else {
//没有权限
}
authorizationInfo.addStringPermissions(permissions);
return authorizationInfo;
}
/**
* 认证
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String t = (String) token.getPrincipal();
//根据token获取userId
Long userId = null;
if ("admin".equals(t)) {
userId = 0L;
} else if ("user".equals(t)) {
userId = 1L;
} else {
//未获取到userId,抛出异常,
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(userId, token.getCredentials(), getName());
}
}
7.最后将以上创建的类注入到配置文件ShiroConfig即可
主要是禁用缓存、不创建Session、关闭Session验证、关闭Session存储、注入配置拦截器、开启权限校验注解等。
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.*;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionManager;
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.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/**
* 注入自定义Realm,禁用缓存
*/
@Bean
public StatelessRealm statelessRealm() {
StatelessRealm statelessRealm = new StatelessRealm();
statelessRealm.setCachingEnabled(false);
return statelessRealm;
}
/**
* 注入SubjectFactory,不创建session
*/
@Bean
public SubjectFactory subjectFactory() {
return new StatelessDefaultSubjectFactory();
}
/**
* 注入SessionManager,关闭Session验证
*/
@Bean
public SessionManager sessionManager() {
DefaultSessionManager defaultSessionManager = new DefaultSessionManager();
defaultSessionManager.setSessionValidationSchedulerEnabled(false);
return defaultSessionManager;
}
/**
* 注入SessionStorageEvaluator,关闭Session存储
*/
@Bean
public SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
return defaultSessionStorageEvaluator;
}
/**
* 注入SecurityManager
*/
@Bean
public SecurityManager securityManager(
StatelessRealm statelessRealm, SessionStorageEvaluator sessionStorageEvaluator,
SubjectFactory subjectFactory, SessionManager sessionManager) {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
defaultSecurityManager.setRealm(statelessRealm);
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
defaultSecurityManager.setSubjectDAO(defaultSubjectDAO);
defaultSecurityManager.setSubjectFactory(subjectFactory);
defaultSecurityManager.setSessionManager(sessionManager);
return defaultSecurityManager;
}
/**
* 拦截器配置
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//添加自定义的拦截器
Map<String, Filter> filters = new HashMap<>();
filters.put("statelessAuthc", new StatelessAuthcFilter());
shiroFilterFactoryBean.setFilters(filters);
//设置自定义的拦截器,拦截所有请求,方法一
Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
// 登录接口不进行token校验
filterChainDefinitionMap.put("/user/login", "anon");
// 其他接口进行token校验
filterChainDefinitionMap.put("/**", "statelessAuthc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//方法二:shiroFilterFactoryBean.setFilterChainDefinitions("/**=statelessAuthc");
return shiroFilterFactoryBean;
}
/**
* 拦截器注册
*/
@Bean
public FilterRegistrationBean delegatingFilterProxy() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
/**
* *
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* *
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)
* 和AuthorizationAttributeSourceAdvisor)即可实现此功能
* *
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
8.测试
8.1访问登录接口,获取token
image8.2使用token=admin,访问任意接口
image8.3使用token=user,访问create接口
image8.4使用token=user,访问read接口
image8.5使用token=didi(即无任何权限的token),访问read接口
image认证缓存和权限缓存
引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.7.0</version>
</dependency>
shiro配置中开启缓存
/**
* 注入自定义Realm
*/
@Bean
public ShiroRealm ShiroRealm() {
ShiroRealm ShiroRealm = new ShiroRealm();
// 开启全局缓存
ShiroRealm.setCachingEnabled(true);
// 开启授权缓存
ShiroRealm.setAuthorizationCachingEnabled(true);
//设置授权缓存名字方便查找
ShiroRealm.setAuthorizationCacheName("authorizationCache");
// 关闭认证缓存
ShiroRealm.setAuthenticationCachingEnabled(false);
// ShiroRealm.setAuthenticationCacheName("authenticationCache");
ShiroRealm.setCacheManager(new EhCacheManager());
return ShiroRealm;
}
获取认证或授权信息
@Autowired
private ShiroRealm shiroRealm;
public void test(){
// 获取授权信息
Cache<Object, AuthorizationInfo> cache = shiroRealm.getAuthorizationCache();
AuthorizationInfo authorizationInfo = cache.get(SecurityUtils.getSubject().getPrincipals());
}
网友评论