shiro是什么
用于进行加密,认证授权的java安全框架
核心对象:
Subject:用户对象,封装信息
SecurityManager:管理所有用户
Realm:连接数据,进行用户认证和授权
Jwt是什么
JWT全称JSON Web Token,是javaweb官方提供的一种封装用户数据,保证权限访问的字符串。
详情见:https://blog.csdn.net/m0_51433562/article/details/119609637?spm=1001.2014.3001.5502
原理讲解
知道了Shiro和Jwt是什么玩意儿之后,那么接下来我们要探究如何使用Shiro和Jwt完成用户认证和授权的呢?
认证(Authentication)和授权(Authorization)
首先我们先弄懂认证和授权到底有什么区别,平常我们都是将认证授权两个词语连在一起进行讨论,实际上认证和授权是两个完全不同的概念。
举个很简单的例子,我们去网吧上网的时候,网管首先会让你出示你的身份证件,核实你的身份。确定你是成年人之后,网管会激活你的号码或者会员,这样你就可以在网吧内入座了。
在上述的例子中,网管核实你是否为成年人就是一个认证的过程。认证通过之后,网管激活号码,你获得在网吧内就做的权利,这就是一个授权的过程。相信通过这个案例,大家对于认证和授权应该有了理解。
Shiro的认证和授权
那么在shiro安全框架中又是怎样实现认证和授权的呢?
在javaWeb开发中,我们会遇到这样的场景,某些页面或者接口是只有特定的用户可以进行访问和调用的,比如VIP视频只允许VIP用户观看,后台管理接口只允许管理员登录。此时shiro就能很好的帮助我们完成这个需求。
分析上图:
- 客户端发送携带JwtToken的请求到后端,会根据Sercurity Manager配置的拦截器进行判断,进入不同的Filter过滤器中(以JwtFilter为例)。
- Filter过滤器会对请求进行拦截,获取请求携带的Token。在过滤器中创建subject对象,这相当与一个用户对象,利用subject对象并携带从客户端获取的token,调用Sercurity Manager,将subject对象统一交给Sercurity Manager去处理。(注:原生shiro中subject对象会对传递来的username和password进行封装形成token,因为我们采用的是jwt,传来的就是token,所以不必封装)。
- subject对象进入Sercurity Manager后,会将携带的Token交给JwtRealm的认证授权程序进行处理,认证通过后,放可执行controller中的请求,否则会抛出异常。
代码编写
了解了shiro和JWT是如何共同实现认证和授权之后,就可以很轻松的完成代码的编写。先介绍我们要用到的类
- ShiroCofig:Shiro的配置类,用于配置subject,securityManager和realm。
- JwtRealm:自定义的Realm对象,用于连接数据,进行认证和授权。
- JwtToken:自定义的token类,用以代替shiro原生的UsernamePasswordToken
- JwtDefaultSubjectFactory(可选):自定义的subjectFactory,继承于DefaultSubjectFactory,用于生产subject对象。
- JwtFilter:需要进行jwt认证的API接口经过的过滤器。
- CommonFilter:不需要进行jwt认证的API接口经过的过滤器。
- JwtUtils:JWT的工具类,用于decode,encode和识别token(具体代码在前面提到的博客中有)
ShiroConfig
ShiroConfig用于进行Shiro的相关配置,主要包括ShiroFilterFactoryBean、DefaultWebSecurityManager和Realm的配置。
- ShiroFilterFactoryBean:用来生产subject,配置过滤器和拦截路由。
- DefaultWebSecurityManager:获得SecurityManager,对subject进行统一管理。还可以进行Session Dao和Session Manager等的配置(相关配置,本文章省略)
- Realm:放回自定义的Realm对象
@Configuration
public class ShiroConfig {
//三大核心对象:Subject、SecurityManager、Realm
//告诉shiro不创建内置的session
@Bean
public SubjectFactory subjectFactory(){
return new JwtDefaultSubjectFactory();
}
//3、ShiroFilterFactoryBean->Subject subject是用户主题,进入到securitymanager中
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//关联securityManager
bean.setSecurityManager(getDefaultWebSecurityManager());
//添加内置过滤器
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("anon",new CommonFilter());
filterMap.put("jwt",new JwtFilter());
bean.setFilters(filterMap);
//添加拦截器,对路由进行限制
Map<String, String> filterRuleMap = new LinkedHashMap<>();
/*
anon:无需认证可以直接访问
auth:必须认证才能访问
user:必须拥有 记住我功能才能用
perms:拥有对某个资源的权限才能访问
role:拥有某个角色权限才能访问
*/
filterRuleMap.put("/treasure/user/login","anon");
filterRuleMap.put("/treasure/user/getUserInfoByEmail/**","jwt");
bean.setFilterChainDefinitionMap(filterRuleMap);
return bean;
}
//2、DefaultWebSecurityManager->SecurityManager 管理所有用户,利用realm完成数据连接
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联realm
securityManager.setRealm(realm());
return securityManager;
}
//1、Realm->Realm 需要自定义一个Realm用于存储数据,这里使用jwtRealm
@Bean
public Realm realm() {
return new JwtRealm();
}
}
JwtRealm
自定义的Realm对象,该对象继承于AuthorizingRealm,实现了Shiro具体认证和授权的方法。doGetAuthenticationInfo用于授权,doGetAuthorizationInfo用于认证。
另外,重写了supports方法,表示该Realm只用于识别JwtToken。
JwtRealm最终会配置为ShiroConfig中Realm的返回对象,并于SecurityManager进行关联。
@Component
public class JwtRealm extends AuthorizingRealm {
@Autowired
private JwtUtils jwtUtils;
/*
* 多重写一个support
* 标识这个Realm是专门用来验证JwtToken
* 不负责验证其他的token(UsernamePasswordToken)
* */
@Override
public boolean supports(AuthenticationToken token) {
//这个token就是从过滤器中传入的jwtToken
return token instanceof JwtToken;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String jwt = (String) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRole("user");
return authorizationInfo;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String jwt = (String) authenticationToken.getPrincipal();
if(!jwtUtils.isVerify(jwt)){
throw new IncorrectCredentialsException("Authorization token is invalid");
}
return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
}
}
JwtToken
JwtToken是定义的一个Token类,继承了AuthenticationToken类,实现getPrincipal和getCredentials方法,(这两个方法本来是用于获取token中的信息,和识别token的,但JwtUtils已经为我们提供了这样的方法,所以这两个方法对于jwtToken没有意义)。用于将客户端传来的token进行封装,便于Realm识别token类型,进行认证和授权。
/*
JwtToken代替原生的UsernamePasswordToken
*/
public class JwtToken implements AuthenticationToken {
private String jwt;
public JwtToken(String jwt) {
this.jwt = jwt;
}
//返回原来的字符串,解析交给JwtUtils实现
@Override
public Object getPrincipal() {
return jwt;
}
//返回原来的字符串,解析交给JwtUtils实现
@Override
public Object getCredentials() {
return jwt;
}
}
JwtDefaultSubjectFactory
继承于DefaultSubjectFactory类,重写了生产工厂,关闭Shiro的session存储
public class JwtDefaultSubjectFactory extends DefaultSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
//不创建shiro内部的session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
JwtFilter
JwtFilter定义为需要进行jwt认证的API结果经过的过滤器,在这个过滤器中,取出API请求中包含的JWT字符串,封装为jwtToken对象,同时创建一个subject,调用login方法进行认证和授权。JwtFilter继承于AccessControlFilter类,实现isAccessAllowed和onAccessDenied方法。
//需要认证的url经过该过滤器
public class JwtFilter extends AccessControlFilter {
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpRequest = (HttpServletRequest) request;
httpResponse.setHeader("Access-Control-Allow-Origin", httpRequest.getHeader("Origin"));
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Content-Type","application/json;charset=UTF-8");
if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpResponse.setHeader("Access-Control-Allow-Methods", httpRequest.getMethod());
httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
return true;
}
return super.preHandle(request, response);
}
/*
* 1. 返回true,shiro就直接允许访问url
* 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
* */
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
String jwt = ((HttpServletRequest) servletRequest).getHeader("Authorization");
if(jwt != null){
JwtToken jwtToken = new JwtToken(jwt);
//这里getSubject方法实际上就是获得一个subject
//与原生shiro不同的地方在于没有对username和password进行封装
//直接使用jwt进行认真,login方法实际上就是交给Realm进行认证
try{
getSubject(servletRequest,servletResponse).login(jwtToken);
}catch (Exception e){
e.printStackTrace();
return false;
}
return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
//直接设置401 未认证
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}
CommonFilter
CommonFilter是不需要进行认证的API接口经过的过滤器,只需要进行跨域处理
// 不需要进行认证的url经过该过滤器
public class CommonFilter extends AnonymousFilter {
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpRequest = (HttpServletRequest) request;
httpResponse.setHeader("Access-Control-Allow-Origin", httpRequest.getHeader("Origin"));
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Content-Type","application/json;charset=UTF-8");
if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpResponse.setHeader("Access-Control-Allow-Methods", httpRequest.getMethod());
httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
return true;
}
return super.preHandle(request, response);
}
}
写了这么多类,大家可能有点懵,实际上仔细看看代码里面的注释还是很容易理解的。为了更清晰的描述之间的关系,见下图。