spring boot Shiro JWT整合
一个api要支持H5, PC和APP三个前端,如果使用session的话对app不是很友好,而且session有跨域攻击的问题,所以选择了JWT
1.导入依赖包
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.2.0</version> </dependency>
2.自定义JWTToken
import org.apache.shiro.authc.AuthenticationToken; public class JwtToken implements AuthenticationToken { private String token; public JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
工具类
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.DecodedJWT; import java.io.UnsupportedEncodingException; import java.util.Date; public class JwtUtils { // 过期时间30天 private static final long EXPIRE_TIME = 24 * 60 * 30 * 1000; /** * 校验token是否正确 * * @param token 密钥 * @param username 登录名 * @param password 密码 * @return */ public static boolean verify(String token, String username, String password) { try { Algorithm algorithm = Algorithm.HMAC256(password); JWTVerifier verifier = JWT.require(algorithm).withClaim("userName", username).build(); DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception e) { return false; } } /** * 获取登录名 * * @param token * @return */ public static String getUsername(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("userName").asString(); } catch (JWTDecodeException e) { return null; } } /** * 生成签名 * * @param username * @param password * @return */ public static String sign(String username, String password) { try { // 指定过期时间 Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(password); return JWT.create() .withClaim("userName", username) .withExpiresAt(date) .sign(algorithm); } catch (UnsupportedEncodingException e) { return null; } } }
3.自定义realm
import com.system.authorization.model.JwtToken; import com.system.authorization.model.MzUser; import com.system.authorization.service.MzUserService; import com.system.authorization.utils.JwtUtils; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import java.util.Set; public class JwtShiroRealm extends AuthorizingRealm { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private MzUserService mzUserService; /** * 使用JWT代替原生Token * @param token * @return */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } //权限验证 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { logger.info("doGetAuthorizationInfo:" + principalCollection.toString()); String userName = JwtUtils.getUsername(principalCollection.toString()); //获取权限数据 Set<String> permissions = mzUserService.getPermissionByUserName(userName); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setStringPermissions(permissions); return simpleAuthorizationInfo; } /** * 身份认证:Authentication 用来验证用户身份 * 默认使用此方法进行用户名正确与否验证,错误抛出异常 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String token = authenticationToken.getPrincipal().toString(); System.out.println("Realm 验证:"+token); String userName = JwtUtils.getUsername(token); System.out.println("Realm 验证用户名:"+userName); MzUser mzUser = mzUserService.queryByUserName(userName); if (mzUser == null) { throw new AuthenticationException("token验证失败,权限不足"); } if (!JwtUtils.verify(token, userName, mzUser.getPassword())) { throw new UnknownAccountException("token验证失败,权限不足"); } return new SimpleAuthenticationInfo(token, token, "realm"); } }
4.自定义filter
import com.system.authorization.model.JwtToken; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.apache.shiro.web.util.WebUtils; 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; public class JwtAuthFilter extends BasicHttpAuthenticationFilter { private Logger logger = LoggerFactory.getLogger(this.getClass()); // 登录标识 private static String LOGIN_SIGN = "x-auth-token"; /** * 检测用户是否登录 * 检测header里面是否包含Authorization字段即可 * * @param request * @param response * @return */ @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest httpRequest = WebUtils.toHttp(request); String authorization = httpRequest.getHeader(LOGIN_SIGN); return StringUtils.isNoneBlank(authorization); } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpRequest = WebUtils.toHttp(request); String token = httpRequest.getHeader(LOGIN_SIGN); JwtToken jwtToken = new JwtToken(token); //提交给realm进行登录,如果错误会怕熬出异常并被捕获,如果没有抛出异常则返回true getSubject(request, response).login(jwtToken); return true; } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { System.out.println("开始jwt 校验"); //如果不是登录请求 if (isLoginAttempt(request, response)) { try { executeLogin(request, response); } catch (Exception e) { // throw new TSharkException("登录权限不足!", e); throw new UnknownAccountException("token验证失败,权限不足"); } } System.out.println("jwt 校验通过"); return true; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { HttpServletResponse httpResponse = WebUtils.toHttp(response); httpResponse.setCharacterEncoding("UTF-8"); httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.setStatus(org.apache.http.HttpStatus.SC_UNAUTHORIZED); System.out.println("token验证失败,没权限访问"); return false; } /** * 对跨域提供支持 * * @param request * @param response * @return * @throws Exception */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) 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")); // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
授权过滤器
import org.apache.http.HttpStatus; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.authz.AuthorizationFilter; import org.apache.shiro.web.util.WebUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class RolesAndPermissionFilter extends AuthorizationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { System.out.println("开始Roles permission校验"); //获取接口请求地址 String path = WebUtils.toHttp(request).getRequestURI(); Subject subject = getSubject(request, response); //数据库中存储的是接口的请求地址,此处验证当前请求的接口地址,当前登录的用户是否存在,如果存在则通过验证 if (subject.isPermitted(path)) return true; System.out.println("roles permission校验未通过"); return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { HttpServletResponse httpResponse = WebUtils.toHttp(response); httpResponse.setCharacterEncoding("UTF-8"); httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.setStatus(HttpStatus.SC_UNAUTHORIZED); return false; } }
5.配置信息,注入spring容器
import com.system.authorization.filter.JwtAuthFilter; import com.system.authorization.filter.RolesAndPermissionFilter; import com.system.authorization.realm.JwtShiroRealm; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.LifecycleBeanPostProcessor; 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.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import javax.servlet.Filter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @Configuration @ConditionalOnWebApplication public class ShiroConfig { @Bean public Realm jwtShiroRealm() { return new JwtShiroRealm(); } @Bean public SecurityManager securityManager() { DefaultSecurityManager defaultSecurityManager = new DefaultWebSecurityManager(); defaultSecurityManager.setRealm(jwtShiroRealm()); // 关闭自带session DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator(); evaluator.setSessionStorageEnabled(false); DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); subjectDAO.setSessionStorageEvaluator(evaluator); defaultSecurityManager.setSubjectDAO(subjectDAO); return defaultSecurityManager; } @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); //将自定义的过滤器注入 Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("jwt", new JwtAuthFilter()); filterMap.put("permission", new RolesAndPermissionFilter()); factoryBean.setFilters(filterMap); factoryBean.setSecurityManager(securityManager); //定义过滤规则 Map<String, String> filterRuleMap = new LinkedHashMap<>(); //所有的请求都必须经过jwt,permission过滤器 filterRuleMap.put("/**", "jwt,permission"); //登录接口可以不做验证 filterRuleMap.put("/mz/user/login", "anon"); factoryBean.setFilterChainDefinitionMap(filterRuleMap); //设置登录页面,主页面,验证失败页面 factoryBean.setLoginUrl("https://www.baidu.com"); factoryBean.setSuccessUrl("https://www.cnblogs.com/gyli20170901/"); factoryBean.setUnauthorizedUrl("/403"); return factoryBean; } @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }