shiro是我接触关于安全认证这块最早的东西,以前单纯的写代码没深入的时候(非科班出身)就对这个感到很新奇,简简单单的对web.xml多了个配置(以前的web项目里),就能实现用户接口进行拦截,实用又简单,这次就正好是有了一定了解后对它进行一次自我总结,不好或者有片面的可以留言我来补充:
一.理解shiro的基础就需要先了解什么是filter(过滤器),而理解过滤器就需要了解过滤器和拦截器的区别,自我总结2点差别如 下:
1.拦截器是基于java反射机制的,过滤器是基于函数回调;过滤器依赖serlevt容器,拦截器依赖bean对象;导致拦截器可以调用内部任意接口(但自身限制在controller层),而过滤器只在serlevt容器初始化的时候调用一次(过滤适用于所有匹配的接口)
2假设serlevt应用服务是tomcat,那么对应的关系是:过滤器在前,然后是serlevt容器创建后,然后才是拦截器的种种对接口的限制
3filter常用业务场景都是在对数或者请求地址进行分发路由(如网关),Interceptor(或者通常shandler)
不好意思工作中此文章写了一半停了(忙其他的去了),耽误了好几天,更新继续!
虽然Apache官网已整合了springboot,但是对实际项目中的一些问题,类如验证码、密码错误次数限制,错误自定义没有明确的说明(也有可能是我没仔细研究查看),感兴趣的自己可以找找 https://shiro.apache.org/spring-boot.html ,我这里就没用整合包,直接实用shiro和spring整合包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
第一步,配置shiroConfig
1创建shiroConfig类,增加shirofilter过滤器
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//Shiro的核心安全接口,这个属性是必须的
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new LinkedHashMap<>();
//过滤所有请求(专为前后分离添加的过滤请求)
filterMap.put("custom", new ShiroUserFilter());
shiroFilterFactoryBean.setFilters(filterMap);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 过滤链定义,从上向下顺序执行,一般将 / ** 放在最为下边:这是一个坑呢,一不小心代码就不好使了;
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/login/auth", "anon");
filterChainDefinitionMap.put("/login/logout", "anon");
filterChainDefinitionMap.put("/error", "anon");
// 使用该过滤器过滤所有的链接
filterChainDefinitionMap.put("/**","custom");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
2加载securityManager的bean
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
return securityManager;
}
3加载自定义ReaLm类
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
return userRealm;
}
4开启注解所需的bean
/**
* Shiro生命周期处理器
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
二重写Realm类(doGetAuthorizationInfo验证用户身份,authcToken在登陆时候设定)
public class UserRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//todo
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { //todo
}
}
三.登陆登出方法(其中/login/auth必须在过滤器中设置anon匿名可进入参数)
RestController
@Api(tags = "认证登陆登出接口")
public class LoginController {
@PostMapping("/login/auth")
@ApiOperation(value = "认证接口")
public Response login(@RequestBody User user) {// 登录测试
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserCode(),user.getLoginPwd());
try {
currentUser.login(token);
return new Response(CodeEnum.SUCCESS, null);
} catch (IncorrectCredentialsException e) {
//密码错误
return new Response(CodeEnum.AUTH_UNMATCH, null);
}
catch (UnknownAccountException e) {
//用户不存在
return new Response(CodeEnum.USER_IS_NOT_EXISTS, null);
}
catch (DisabledAccountException e) {
//用户已停用
return new Response(CodeEnum.STOP_USE, null);
} catch (ExcessiveAttemptsException e) {
//用户被锁定
return new Response(CodeEnum.AUTH_LOCKED, null);
}catch (Exception e){
//其他错误
return new Response(CodeEnum.ERR_STATUS, null);
}
}
@ApiOperation(value = "退出接口")
@GetMapping("/logout")
public Response logOut() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return new Response(CodeEnum.SUCCESS, null);
}
}
当 currentUser.login(token)执行到时就调用userRealm类的doGetAuthenticationInfo(AuthenticationToken authcToken)来现实认证功能,具体实现功能根据数据库设计而已,其主要SimpleAuthenticationInfo(认证信息)
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userEntity.getUserCode(),
userEntity.getLoginPwd(),
ByteSource.Util.bytes(salt),
// ByteSource.Util.bytes("salt"), //salt=username+salt, //采用明文访问时,不需要此句
getName()
);
因为我这里是将用户大部分信息放在session作为缓存中,所以认证信息只保留了当前用户名
//将用户信息放入session中
SecurityUtils.getSubject().getSession().setAttribute(Constants.SESSION_USER_INFO(自定义一个字符key), userEntity(用户信息));
四,细节问题(个人认为比较大的几个注意问题)
1自定义异常问题:
doGetAuthenticationInfo(AuthenticationToken authcToken)有AuthenticationException,其下异常可自定义(比如我在登陆的时候就做了catch模块对不同异常不同定义)
2在将seesion放入用户信息之前,最好将密码信息(如密码,盐等)进行清空
//session中去掉密码和盐,防止泄露
userEntity.setLoginPwd("");
userEntity.setSalt("");
3用户角色信息,可以放在登陆认证中,也可以放在认证拦截器方法时,主要方法:
authorizationInfo.addRoles(roles);
五跨域问题
filterMap.put("custom", new ShiroUserFilter())在拦截器过滤链上增加一个shiroUserFilter过滤器
import com.alibaba.fastjson.JSONObject;
import com.wjx.product.els.config.common.CodeEnum;
import com.wjx.product.els.config.common.Response;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author zhouxl
* 重写shiro的filter
*/
public class ShiroUserFilter extends UserFilter {
/**
* 在访问过来的时候检测是否为OPTIONS请求,如果是就直接返回true
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpRequest = (HttpServletRequest) request;
if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
setHeader(httpRequest,httpResponse);
return true;
}
return super.preHandle(request,response);
}
/**
* 该方法会在验证失败后调用,这里由于是前后端分离,后台不控制页面跳转
* 因此重写改成传输JSON数据
*/
@Override
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
saveRequest(request);
setHeader((HttpServletRequest) request,(HttpServletResponse) response);
PrintWriter out = response.getWriter();
// 重写没有认证通过返回的状态码和异常
out.println(JSONObject.toJSONString(new Response<>(CodeEnum.AUTH_NOTLOGIN, null)));
out.flush();
out.close();
}
/**
* 为response设置header,实现跨域
*/
private void setHeader(HttpServletRequest request,HttpServletResponse response){
//跨域的header设置
response.setHeader("Access-control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", request.getMethod());
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
//防止乱码,适用于传输JSON数据
response.setHeader("Content-Type","application/json;charset=UTF-8");
response.setStatus(HttpStatus.OK.value());
}
}