Apache Shiro :是一个强大且易用的Java安全框架,执行身份认证,授权,密码和会话管理,核心组件:Subject,SecurityManager和Realms;
JWT:JSON Web Token是一种流行的跨域身份验证解决方案,主要是用于客户端与用户端之间信息的传递;
SpringBoot:目前Java主流的一个开发框架,不仅集成Spring框架原有的优秀特性,而且通过简化配置来进一步简化Spring应用的整个搭建和开发过程。
https://download.csdn.net/download/weixin_40736233/16334422
<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>
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
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;
}
}
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;
}
}
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);
}
}
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);
}
}
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;
}
}
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;
}
}
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,"访问接口成功");
}
}
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进行测试