应用场景:html
因为项目后端管理系统不仅是一个服务,并且不仅在Web端运行,还会在移动端App使用,想要使用JWT方式进行无状态的RESTful API交互,也就是登陆后生成token并返回给前端,前端每次请求时都在请求头里面添加token,后端验证有效性。因此Shiro自带的Session要禁用掉,同时要从新写JWT的过滤器。前端
使用shiro+ssm+jwt的一些前期准备:java
禁用session
JWT实现工具,这个网上能够找到不少,这里就不贴代码了
自定义realm,继承AuthorizingRealm,重写认证和受权两个方法
自定义filter,继承AccessControlFilter,重写方法isAccessAllowed,onAccessDeniedweb
1、首先,禁用shiro的session,先添加shiro的依赖到pomspring
<!--Shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</version> </dependency>
2、禁用 session数据库
package com.tg.higo.shiro; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.SubjectContext; import org.apache.shiro.web.mgt.DefaultWebSubjectFactory; public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory { @Override public Subject createSubject(SubjectContext context) { // 不建立 session context.setSessionCreationEnabled(false); return super.createSubject(context); } }
配置
applicationContext-shiro.xml
<bean id="subjectFactory" class="com.tg.higo.shiro.JwtDefaultSubjectFactory"></bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- 配置 realm --> <property name="realm" ref="bosRealm"/> <property name="subjectFactory" ref="subjectFactory"/> </bean>
3、 重写filter 过滤器apache
package com.tg.higo.shiro; import com.tg.higo.common.utils.StringUtil; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.UnsupportedEncodingException; import java.net.URLEncoder; public class JwtFilter extends AccessControlFilter { /** * 日志对象 */ protected Logger logger = LoggerFactory.getLogger(AdminShiroRealm.class); /* * 1. 返回true,shiro就直接容许访问url * 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否容许访问url * */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { logger.warn("isAccessAllowed 方法被调用"); //这里先让它始终返回false来使用onAccessDenied()方法 return false; } /** * 返回结果为true代表登陆经过 */ @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { logger.warn("onAccessDenied 方法被调用"); //这个地方和前端约定,要求前端将jwtToken放在请求的Header部分 //因此之后发起请求的时候就须要在Header中放一个Authorization,值就是对应的Token HttpServletRequest request = (HttpServletRequest) servletRequest; String jwt = getTokenFromRequest(request); if(StringUtil.isBlank(jwt)){ onLoginFail(servletResponse); //调用下面的方法向客户端返回错误信息 return false; } // String jwt = request.getHeader("Authorization"); logger.info("请求的 Header 中藏有 jwtToken {}", jwt); JwtToken jwtToken = new JwtToken(jwt); /* * 下面就是固定写法 * */ try { // 委托 realm 进行登陆认证 //因此这个地方最终仍是调用JwtRealm进行的认证 getSubject(servletRequest, servletResponse).login(jwtToken); //也就是subject.login(token) } catch (Exception e) { // e.printStackTrace(); logger.info(e.getMessage()); onLoginFail(servletResponse); //调用下面的方法向客户端返回错误信息 return false; } return true; //执行方法中没有抛出异常就表示登陆成功 } // 从请求中获取 token private String getTokenFromRequest(ServletRequest request) { HttpServletRequest req = (HttpServletRequest) request; return req.getHeader("Token"); } //登陆失败时默认返回 401 状态码 private void onLoginFail(ServletResponse response) throws IOException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.getWriter().write("login error"); } }
4、 重写Realm ,定义认证,受权后端
package com.tg.higo.shiro; import com.tg.higo.common.utils.JwtUtils; import com.tg.higo.dao.UserDtoMapper; import com.tg.higo.exception.RRException; import com.tg.higo.model.UserDto; import com.tg.higo.model.dto.User; import org.apache.commons.lang.StringUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationException; 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 org.apache.shiro.util.ByteSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Resource; import java.util.HashSet; import java.util.Set; public class AdminShiroRealm extends AuthorizingRealm { /** * 日志对象 */ protected Logger logger = LoggerFactory.getLogger(AdminShiroRealm.class); /* * 多重写一个support * 标识这个Realm是专门用来验证JwtToken * 不负责验证其余的token(UsernamePasswordToken) * */ @Override public boolean supports(AuthenticationToken token) { //这个token就是从过滤器中传入的jwtToken return token instanceof JwtToken; } //认证 //这个token就是从过滤器中传入的jwtToken @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String jwt = (String) token.getCredentials(); // if (jwt == null) { // // throw new NullPointerException("jwtToken 不容许为空"); // } //判断 // JwtUtils jwtUtil = new JwtUtils(); if (!JwtUtils.validToken(jwt)) { throw new UnknownAccountException(); } //下面是验证这个user是不是真实存在的 String username = (String) JwtUtils.decodeToken(jwt).getAccount();//判断数据库中username是否存在 // log.info("在使用token登陆"+username); return new SimpleAuthenticationInfo(jwt,jwt,getName()); //这里返回的是相似帐号密码的东西,可是jwtToken都是jwt字符串。还须要一个该Realm的类名 } // 受权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { if (principalCollection == null) { throw new AuthorizationException("PrincipalCollection method argument cannot be null."); } User user= JwtUtils.decodeToken(principalCollection.toString()); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // UserDto user = (UserDto) principalCollection.getPrimaryPrincipal(); //从数据库查询角色 Set<String> roleSet = new HashSet<>(); roleSet.add("ghfhgfg"); authorizationInfo.setRoles(roleSet); // //从数据库查询权限 Set<String> permsSet = new HashSet<>(); permsSet.add("/account/update/state"); authorizationInfo.setStringPermissions(permsSet); return authorizationInfo; } }
5、 shiro配置文件,applicationContext-shiro.xml缓存
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd "> <!-- 配置 shiro 核心过滤器:shiroFilter --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- 安全管理器 --> <property name="securityManager" ref="securityManager"/> <!--<!– 未登录跳转的页面 –>--> <!--<property name="loginUrl" value="/login.jsp"/> --> <!--<!– 受权失败跳转的页面 –>--> <!--<property name="unauthorizedUrl" value="/noauth.jsp"/> --> <property name="filters"> <util:map> <entry key="jwt" value-ref="jwtFilter" /> <!--<entry key="adminFilter" value-ref="adminFilter" />--> </util:map> </property> <!-- 配置 url 过滤规则 --> <property name="filterChainDefinitions"> <value> <!-- 所有资源都须要认证才能访问 --> /account/login=anon <!-- 用户受权 --> <!--/account/**=adminFilter--> /timing/zeroTask=anon /**=jwt,roles[ghfhgfg1] <!--/**= roles[ghfhgfg]--> </value> </property> </bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- 配置 realm --> <property name="realm" ref="bosRealm"/> <property name="subjectFactory" ref="subjectFactory"/> <property name="subjectDAO" ref="subjectDAO"/> <!--<property name="sessionManager" ref="sessionManager" />--> </bean> <!--<!– 一、安全管理器 –>--> <!--<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">--> <!--<property name="realm" ref="shiroDbRealm"></property>--> <!--<!– 设置缓存管理器为 ehcache –>--> <!--<property name="cacheManager" ref="shiroEhcacheManager"></property>--> <!--<!– 配置sessionManager,提供session管理 –>--> <!--<property name="sessionManager" ref="sessionManager"></property>--> <!--</bean>--> <!-- 自定义 realm --> <bean id="bosRealm" class="com.tg.higo.shiro.AdminShiroRealm"></bean> <!--<bean id="sessionManager" class="com.tg.higo.shiro.CustomSessionManager"> </bean>--> <bean id="subjectFactory" class="com.tg.higo.shiro.JwtDefaultSubjectFactory"></bean> <bean id ="defaultSessionStorageEvaluator" class="org.apache.shiro.mgt.DefaultSessionStorageEvaluator"> <property name="sessionStorageEnabled" value="false"></property> </bean> <bean id ="subjectDAO" class="org.apache.shiro.mgt.DefaultSubjectDAO"> <property name="sessionStorageEvaluator" ref="defaultSessionStorageEvaluator"/> </bean> <!-- 自定义拦截器 --> <bean id="jwtFilter" class="com.tg.higo.shiro.JwtFilter" /> <!--<bean id="adminFilter" class="com.tg.higo.shiro.AdminAuthenticationFilter" />--> <!--<bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor" id="lifecycleBeanPostProcessor" />--> </beans>
6、总结安全
登陆不在走认证,之后每次要受权的接口,先走认证校验是否有效,再去受权