SpringBoot+Shiro+Jwt整合
前言
Apache Shiro :是一个强大且易用的Java安全框架,执行身份认证,授权,密码和会话管理,核心组件:Subject,SecurityManager和Realms;
JWT:JSON Web Token是一种流行的跨域身份验证解决方案,主要是用于客户端与用户端之间信息的传递;
SpringBoot:目前Java主流的一个开发框架,不仅集成Spring框架原有的优秀特性,而且通过简化配置来进一步简化Spring应用的整个搭建和开发过程。
项目下载链接
https://download.csdn.net/download/weixin_40736233/16334422
目录结构
1.pom.xml依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.5.RELEASE</version> <relativePath/> </parent> <properties> <!--java_JDK版本--> <java.version>1.8</java.version> <!--maven打包插件--> <maven.plugin.version>3.8.1</maven.plugin.version> <!--编译编码UTF-8--> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!--输出报告编码UTF-8--> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <!--shiro版本--> <shiro.version>1.6.0</shiro.version> <!--jwt版本--> <java-jwt.version>3.11.0</java-jwt.version> <!--shiro-redis版本--> <shiro-redis.version>3.1.0</shiro-redis.version> <!--json数据格式处理工具--> <fastjson.version>1.2.75</fastjson.version> </properties> <dependencies> <!--集成springmvc框架并实现自动配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- json --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.75</version> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--JWT--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>${java-jwt.version}</version> </dependency> <!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> <!-- shiro-redis --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>${shiro-redis.version}</version> <exclusions> <exclusion> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> </exclusion> </exclusions> </dependency> <!--commons类--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> </dependencies>
2.application.yml配置
server: port: 7001 servlet: context-path: /sevenhee spring: application: name: shiro redis: host: 127.0.0.1 port: 6379 password: '' jedis: pool: max-active: 8 max-wait: -1 max-idle: 500 min-idle: 0 lettuce: shutdown-timeout: 0 timeout: 2000ms cache: type: redis #自定义属性 custom: jwt: tokenHeader: SevenHee-Token expire_time: 1800000
3.先对我们的处理结果做个统一通用接口返回封装类(Result.Java)
import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * 统一通用接口返回封装类 * * @author xiaosongyue * @date 2021/01/19 09:26:32 */ @Data @NoArgsConstructor public class Result implements Serializable { private static final long serialVersionUID = 1L; /** * 后台是否处理成功(状态) */ private boolean state; /** * 前后端约定的状态码(状态码) */ private int code; /** * 后台响应的信息(处理信息) */ private String message; /** * 后台响应的数据(返回数据) */ private Object data; public static Result success() { Result result = new Result(); result.setState(true); result.setCode(1); result.setMessage("操作成功"); return result; } public static Result success(Object data) { Result result = new Result(); result.setState(true); result.setCode(1); result.setMessage("操作成功"); result.setData(data); return result; } public static Result success(int code, String message) { Result result = new Result(); result.setState(true); result.setCode(code); result.setMessage(message); return result; } public static Result success(int code, String message, Object data) { Result result = new Result(); result.setState(true); result.setCode(code); result.setMessage(message); result.setData(data); return result; } public static Result fail() { Result result = new Result(); result.setState(false); result.setCode(-1); result.setMessage("操作失败"); return result; } public static Result fail(Object data) { Result result = new Result(); result.setState(false); result.setCode(-1); result.setMessage("操作失败"); result.setData(data); return result; } public static Result fail(int code, String message) { Result result = new Result(); result.setState(false); result.setCode(code); result.setMessage(message); return result; } public static Result fail(int code, String message, Object data) { Result result = new Result(); result.setState(false); result.setCode(code); result.setMessage(message); result.setData(data); return result; } }
4.实现shiro的AuthenticationToken接口的,重写Token类型(JwtFilter.java)
import org.apache.shiro.authc.AuthenticationToken; /** * 实现shiro的AuthenticationToken接口的类JwtToken * * @author xiaosongyue * @date 2021/01/21 15:41:20 */ 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; } }
5.编写Jwt的工具类,主要是生成,解析,获取值等功能(JwtUtil.java)
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 lombok.extern.slf4j.Slf4j; import java.util.Date; /** * JwtUtil:用来进行签名和效验Token * * @author xiaosongyue * @date 2021/01/21 15:41:35 */ @Slf4j public class JwtUtil { /** * JWT验证过期时间 EXPIRE_TIME 分钟 */ private static final long EXPIRE_TIME = 30 * 60 * 1000; /** * 校验token是否正确 * * @param token 密钥 * @param secret 用户的密码 * @return 是否正确 */ public static boolean verify(String token, String username, String secret) { try { //根据密码生成JWT效验器 Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username", username) .build(); //效验TOKEN DecodedJWT jwt = verifier.verify(token); log.info("登录验证成功!"); return true; } catch (Exception exception) { log.error("JwtUtil登录验证失败!"); return false; } } /** * 获得token中的信息无需secret解密也能获得 * * @return token中包含的用户名 */ public static String getUsername(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { return null; } } /** * 生成token签名EXPIRE_TIME 分钟后过期 * * @param username 用户名(电话号码) * @param secret 用户的密码 * @return 加密的token */ public static String sign(String username, String secret) { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); // 附带username信息 return JWT.create() .withClaim("username", username) .withExpiresAt(date) .sign(algorithm); } }
6.编写JWT的过滤器(JWTFilter.java)
import com.alibaba.fastjson.JSONObject; import com.xsy.sevenhee.common.Result; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.Filter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * jwt过滤器 * * @author xiaosongyue * @date 2021/01/21 15:40:42 */ @Slf4j public class JwtFilter extends BasicHttpAuthenticationFilter implements Filter { /** * 执行登录 * * @param request * @param response * @return * @throws Exception */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader("SevenHee-Token"); JwtToken jwtToken = new JwtToken(token); // 提交给realm进行登入,如果错误他会抛出异常并被捕获 try { getSubject(request, response).login(jwtToken); // 如果没有抛出异常则代表登入成功,返回true return true; } catch (AuthenticationException e) { return false; } } /** * 执行登录认证 * * @param request * @param response * @param mappedValue * @return */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { try { return executeLogin(request, response); } catch (Exception e) { log.error("JwtFilter过滤验证失败!"); return false; } } /** * 认证失败时,自定义返回json数据 * * @param request 请求 * @param response 响应 * @return boolean* @throws Exception 异常 */ @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { Result result = Result.fail(-1,"认证失败"); Object parse = JSONObject.toJSON(result); response.setCharacterEncoding("utf-8"); response.getWriter().print(parse); return super.onAccessDenied(request, response); } /** * 对跨域提供支持 * * @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); } }
7.shiro配置类(ShiroConfig.java)
tips:主要是用来配置Shiro的相关功能,如拦截过滤路径,密码加密,开启注解,安全管理器
import com.xsy.sevenhee.jwt.JwtFilter; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.mgt.SecurityManager; 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.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; 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; /** * shiro配置 * * @author xiaosongyue * @date 2021/03/30 16:37:11 */ @Configuration public class ShiroConfig { @Autowired private ShiroRealm shiroRealm; /** * shiro过滤器工厂 * * @param securityManager 安全管理器 * @return {@link ShiroFilterFactoryBean} */ @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) { //创建拦截链实例 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //设置安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); //设置组登录请求,其他路径一律自动跳转到这里 shiroFilterFactoryBean.setLoginUrl("/login"); //未授权跳转路径 shiroFilterFactoryBean.setUnauthorizedUrl("/notRole"); //设置拦截链map LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); //放行请求 filterChainDefinitionMap.put("/shiro/getToken", "anon"); //拦截剩下的其他请求 filterChainDefinitionMap.put("/**", "authc"); //设置拦截规则给shiro的拦截链工厂 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); // 添加自己的自定义拦截器并且取名为jwt Map<String, Filter> filterMap = new HashMap<String, Filter>(1); filterMap.put("jwt", new JwtFilter()); shiroFilterFactoryBean.setFilters(filterMap); //拦截链配置,从上向下顺序执行,一般将jwt过滤器放在最为下边 filterChainDefinitionMap.put("/**", "jwt"); //配置拦截链到过滤器工厂 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); //返回实例 return shiroFilterFactoryBean; } /** * 安全管理器 * * @return {@link DefaultWebSecurityManager} */ @Bean public DefaultWebSecurityManager securityManager() { //创建默认的web安全管理器 DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager(); //配置shiro的自定义认证逻辑 defaultSecurityManager.setRealm(shiroRealm); /* * 关闭shiro自带的session,详情见文档 * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29 */ DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); defaultSecurityManager.setSubjectDAO(subjectDAO); //返回安全管理器实例 return defaultSecurityManager; } /** * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions) * @return */ @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } /** * 开启aop注解支持 * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
8.shiro自定义认证逻辑类(ShiroRealm)
tips:自定义的Realm类,主要是用来对用进行用户认证以及权限认证
import com.xsy.sevenhee.jwt.JwtToken; import com.xsy.sevenhee.jwt.JwtUtil; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.HashSet; import java.util.concurrent.TimeUnit; /** * shiro自定义认证逻辑 * * @author xiaosongyue * @date 2021/03/30 16:52:06 */ @Component public class ShiroRealm extends AuthorizingRealm { @Autowired private RedisTemplate redisTemplate; /** * redis过期时间设置 */ @Value("${custom.jwt.expire_time}") private long expireTime; /** * 设置对应的token类型 * 必须重写此方法,不然Shiro会报错 * * @param token 令牌 * @return boolean */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * 授权认证 * * @param principalCollection 主要收集 * @return {@link AuthorizationInfo} */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //权限认证 System.out.println("开始进行权限认证............."); //获取用户名 String token = (String) SecurityUtils.getSubject().getPrincipal(); String username = JwtUtil.getUsername(token); //模拟数据库校验,写死用户名xsy,其他用户无法登陆成功 if (!"xsy".equals(username)) { return null; } //创建授权信息 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //创建set集合,存储权限 HashSet<String> rootSet = new HashSet<>(); //添加权限 rootSet.add("user:show"); rootSet.add("user:admin"); //设置权限 info.setStringPermissions(rootSet); //返回权限实例 return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("开始身份认证....................."); //获取token String token = (String) authenticationToken.getCredentials(); //创建字符串,存储用户信息 String username = null; try { //获取用户名 username = JwtUtil.getUsername(token); } catch (AuthenticationException e) { throw new AuthenticationException("heard的token拼写错误或者值为空"); } if (username == null) { throw new AuthenticationException("token无效"); } // 校验token是否超时失效 & 或者账号密码是否错误 if (!jwtTokenRefresh(token, username, "123")) { throw new AuthenticationException("Token失效,请重新登录!"); } //返回身份认证信息 return new SimpleAuthenticationInfo(token, token, "my_realm"); } /** * jwt刷新令牌 * * @param token 令牌 * @param userName 用户名 * @param passWord 通过单词 * @return boolean */ public boolean jwtTokenRefresh(String token, String userName, String passWord) { String redisToken = (String) redisTemplate.opsForValue().get(token); if (redisToken != null) { if (!JwtUtil.verify(redisToken, userName, passWord)) { String newToken = JwtUtil.sign(userName, passWord); //设置redis缓存 redisTemplate.opsForValue().set(token, newToken, expireTime * 2 / 1000, TimeUnit.SECONDS); } return true; } return false; } }
9.创建测试类(ShiroController.java)
import com.xsy.sevenhee.common.Result; import com.xsy.sevenhee.jwt.JwtUtil; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.TimeUnit; /** * shiro控制器 * * @author xiaosongyue * @date 2021/04/01 14:48:40 */ @RestController @RequestMapping("/shiro") public class ShiroController { @Autowired private RedisTemplate redisTemplate; @Value("${custom.jwt.expire_time}") private long expireTime; @RequestMapping("/getToken") public Result getToken(){ String token = JwtUtil.sign("xsy", "123"); redisTemplate.opsForValue().set(token,token, expireTime*2/100, TimeUnit.SECONDS); return Result.success(token); } @RequiresPermissions("user:admin") @RequestMapping("/test") public Result test(){ System.out.println("进入测试,只有带有令牌才可以进入该方法"); return Result.success(1,"访问接口成功"); } }
10.创建springboot的启动类
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * shiro的应用程序 * * @author xiaosongyue * @date 2021/04/01 14:35:17 */ @SpringBootApplication public class ShiroApplication { public static void main(String[] args) { SpringApplication.run(ShiroApplication.class); } }
11.使用postman进行测试
(1)获取token
(2)携带token访问接口,访问成功
(3)不携带token访问,认证失败