Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能。之前的开发都是在低代码上直接使用Shiro,一般也不需要修改。
在SpringBoot的基础上,只接入Shiro的登录认证还是头一次。看各方文档和代码,不是一大坨概念讲的云里雾里,就是一大坨代码看的眼花缭乱。认证和授权拌在一起,咽又咽不下去,咽了一点也消化不了。
花了一天时间终于从JeecgBoot的代码中拆出了登录认证,又花了一天时间做了个极简版本,现在分享给大家。
一、背景
后端基于SpringBoot,前端基于vue使用antdv的组件,即前后端分离。
二、准备
1、添加Shiro依赖
org.apache.shiro
shiro-spring
1.9.0
2、前端请求
为了简化前端,直接使用Swagger2的API接口文档做前端测试。
Swagger2的接入,请参考之前的博文,接入knife4(3.0.3)。
3、后端接口
后端添加俩测试用的接口,一个接口不需要认证,另一个接口需要认证,代码如下:
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "测试Shiro")
@RestController
@RequestMapping("/demo")
public class DemoRC {
/**
* 这个接口不需要认证
*/
@ApiOperation("Get1")
@GetMapping("/get1")
String get() {
return "get1";
}
/**
* 这个接口需要认证
*/
@ApiOperation("Get2")
@GetMapping("/get2")
String get2() {
return "get2";
}
}
三、接入Shiro
1、添加Shiro配置
配置是过滤器的第一步,需要告诉框架,Shiro要处理什么样的网络请求,代码如下:
import com.example.demo.config.shiro.filters.CustomFilter;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
// Swagger相关地址放入白名单
Map filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/swagger**/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v3/**", "anon");
// 测试接口放入白名单(不需要认证)
filterChainDefinitionMap.put("/demo/get1", "anon");
// 白名单之外的,都得通过该过滤器
Map filterMap = new HashMap(1);
filterMap.put("custom", new CustomFilter());
shiroFilterFactoryBean.setFilters(filterMap);
filterChainDefinitionMap.put("/**", "custom");
// 设置过滤器
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 创建默认安全管理对象
*/
private DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自己的认证员(只有自己知道token与用户名的关系)
securityManager.setRealm(new CustomRealm());
return securityManager;
}
}
其中CustomFilter和CustomRealm是自定义类,下面来定义这俩类。
2、过滤器CustomFilter
import com.example.demo.config.shiro.CustomToken;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
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;
public class CustomFilter extends BasicHttpAuthenticationFilter {
/**
* 是否允许通过,只做最基本的判断
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("ACCESS-TOKEN");
// Token存在,且长度必须大于6
if (token != null && token.length() > 6) {
CustomToken customToken = new CustomToken(token);
// 进一步认证失败,则不允许通过
try {
getSubject(request, response).login(customToken);
} catch (AuthenticationException e) {
return false;
}
return true;
}
return false;
}
/**
* 没通过判断,则返回固定的数据
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
httpServletResponse.getWriter().write("{\"code\":403,\"message\":\"用户未登录,请进行登录\"}");
return false;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
{
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 是否允许发送Cookie,默认Cookie不包括在CORS请求之中。设为true时,表示服务器允许Cookie包含在请求中。
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
}
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
如果是前后端不分离的,不需要实现方法preHandle。
这里要求前端请求的Header中包含“ACCESS-TOKEN”,并且值的长度大于6,根据需要自己修改。
3、CustomToken
自定义一个Token类,以便做进一步的认证,代码如下:
import org.apache.shiro.authc.AuthenticationToken;
public class CustomToken implements AuthenticationToken {
private final String token;
public CustomToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return this.token;
}
@Override
public Object getCredentials() {
return this.token;
}
public String getToken() {
return this.token;
}
}
4、进一步认证CustomRealm
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthenticatingRealm;
public class CustomRealm extends AuthenticatingRealm {
/**
* 告诉框架,CustomToken类型的Token必须要通过当前类的认证
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof CustomToken;
}
/**
* 登录认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
CustomToken customToken = (CustomToken)auth;
// token长度为8才是合法的
if (customToken.getToken().length() != 8) {
throw new AuthenticationException("token错误!");
}
// 从token中得到用户名
String username = customToken.getToken().substring(0, 4);
// 根据用户名得到用户信息
Object userInfo = new String(username);
return new SimpleAuthenticationInfo(userInfo, customToken.getToken(), getName());
}
}
四、测试
1、开启动态参数请求,以便修改Header
打开API接口文档,点击菜单“文档管理” -> “个性化设置”,勾选“开启动态请求参数”,然后刷新页面。如下图:
2、测试
(1)不带Header
点击菜单“测试Shiro”下的“Get1”和“Get2”,分别请求,可以发现Get1能正常请求到数据,Get2则返回了403的固定数据(CustomFilter中写死的)。如下图:
(2)token太短
Get2,添加请求头。请求头名称“ACCESS-TOKEN”,请求头内容“123456”。依然返回403,因为CustomFilter中要求token长度必须大于6,基本判断没通过。如下图:
(3)token位数错误
请求头内容输入“1234567”,依然返回403。CustomRealm中判断token不是8位抛了异常,但CustomFilter中捕捉了该异常。如下图:
(4)认证通过
请求头中输入“12345678”,正确得到了返回结果。如下图:
五、结束
终于,终于弄通了Shiro认证的基本流程,下一步处理登录逻辑生成token,然后进行token认证。