最近看了下shiro这个框架,感觉使用还蛮方便的,话不多说直接上代码。
码云项目地址 : Springboot + shiro + redis + jwt + jpa
新建一个Springboot项目
1.pom.xml加入相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
<exclusions>
<exclusion>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.61</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
2. application.yml配置
server:
#端口号
port: 9002
spring:
# 数据源设置
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/shirotest?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
redis:
password:
timeout: 2000s
3. 实体类及repository
用户表
@Entity
@Data
@Table(name = "t_user")
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UserEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String userName;
String password;
@Transient
String token;
}
权限表
@Data
@Entity
@Table(name="t_role")
public class RoleEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
String roleName;
}
用户权限中间表
@Entity
@Data
@Table(name="t_permission")
public class PermissionEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
Long userId;
Long roleId;
}
Repository
public interface UserRepository extends JpaRepository<UserEntity,Long> {
UserEntity findFirstByUserName(String userName);
}
public interface PermissionRepsitory extends JpaRepository<PermissionEntity,Long> {
List<PermissionEntity> findByUserId(Long userId);
}
4. 配置类
redis配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
/*
设置这些是 当redis的value设置为Jackson2JsonRedisSerializer,导致shiro反序列化时出错
直接使用JdkSerializationRedisSerializer不会出错,但是在RedisDesktopManager中,无法查看保存的数据
*/
ObjectMapper om = new ObjectMapper();
//在反序列化时忽略在JSON字符串中存在,而在Java中不存在的属性
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
redisTemplate.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
我们使用的是JwtToken来代替sessionId,所以创建了AuthToken类
AuthToken
public class AuthToken implements AuthenticationToken {
private String token;
public AuthToken() {
}
public AuthToken(String token) {
this.token = token;
}
public String getToken() {
return token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
AuthRealm 是自定义用户登陆认证、权限设置
- 用户认证:初登陆外其余的请求在header头中必须带token这个标志,我们根据token标志到redis判断。我这里做了单点、登出后的token无效、token验证
- 授权:在用户认证成功后可以给该用户授予业务权限
@Slf4j
@Component
public class AuthRealm extends AuthorizingRealm {
@Autowired
private PermissionRepsitory permissionRepsitory;
//必须重写,不然会报错
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof AuthToken;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.debug("开始执行授权操作.......");
System.out.println("调用了授权方法");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//如果身份认证的时候没有传入User对象,这里只能取到userName
//也就是SimpleAuthenticationInfo构造的时候第一个参数传递需要User对象
UserEntity user = (UserEntity) principalCollection.getPrimaryPrincipal();
Long userId = user.getId();
List<PermissionEntity> list = permissionRepsitory.findByUserId(userId);
if(!list.isEmpty()){
list.forEach(o ->{
authorizationInfo.addStringPermission(o.getRoleId().toString());
});
}
return authorizationInfo;
}
//验证用户
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("验证开始。。。");
String token = (String) authenticationToken.getCredentials();
Long userId = TokenUtil.getField(token,"userId",Long.class);
if(!RedisUtil.hasKey(ShiroConstant.LOGIN_SHIRO_CACHE + userId)){
throw new AuthenticationException("redis无该用户,登出或被删除,请重新登陆!");
}
UserEntity user = (UserEntity) RedisUtil.get(ShiroConstant.LOGIN_SHIRO_CACHE + userId);
if(!user.getToken().equals(token)){
throw new AuthenticationException("token不等错误!请重新登陆");
}
try{
TokenUtil.verify(token,user.getUserName(),user.getId());
}catch (JWTVerificationException e){
throw new AuthenticationException("token验证出错," + e.getMessage());
}
return new SimpleAuthenticationInfo(user, token, this.getName());
}
}
AuthFilter(自定义拦截器)
@Slf4j
public class AuthFiter extends AuthenticatingFilter{
/**
* 生成自定义token
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isEmpty(token)) {
return null;
}
return new AuthToken(token);
}
/**
* 步骤1.拦截请求并验证token,成功则进行授权,否则进入onAccessDenied方法
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
AuthToken jwtToken = (AuthToken)this.createToken(request,response);
if(jwtToken != null){
try {
String token = jwtToken.getToken();
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
//判断是否要更新token
String refreshToken = this.refreshToken(token);
if(!StringUtils.isEmpty(refreshToken)){
log.info("更新token时间!!!!!");
UserEntity user = (UserEntity) SecurityUtils.getSubject().getPrincipal();
user.setToken(refreshToken);
//更新redis中用户对象的token
RedisUtil.set(ShiroConstant.LOGIN_SHIRO_CACHE + user.getId(),user);
//将响应结果加上更新后的token
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("token", refreshToken);
httpServletResponse.setHeader("Access-Control-Expose-Headers", "token");
}
return true;
} catch (AuthenticationException e) {
log.error("登陆失败",e);
return false;
}
}else{
return false;
}
}
/**
* 步骤2,若验证成功则进行业务操作,false直接返回。我这边的流程在 isAccessAllowed已经处理完了,所以这里直接返回false。
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
return false;
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
String token = httpRequest.getHeader("token");
//如果header中不存在token,则从参数中获取token
if (StringUtils.isEmpty(token)) {
token = httpRequest.getParameter("token");
}
return token;
}
/**
* 更新token
*/
private String refreshToken(String token){
String sign = null;
DecodedJWT jwt = JWT.decode(token);
//获取过期时间
Date exDate = jwt.getExpiresAt();
//比较过期时间
boolean refesh = (exDate.getTime() - System.currentTimeMillis()) < TokenUtil.USED_TIME;
if(refesh){
//获取token中的数据
Long userId = TokenUtil.getField(token,"userId",Long.class);
String userName = TokenUtil.getField(token,"userName",String.class);
sign = TokenUtil.sign(userName,userId);
}
return sign;
}
}
ShiroConfig(shiro配置)
@Configuration
public class ShiroConfig {
@Bean(name="shiroFilter")
public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//自定义过滤器
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("authc",new AuthFiter());
shiroFilterFactoryBean.setFilters(filterMap);
LinkedHashMap<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
//注意过滤器配置顺序 不能颠倒
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/user/login", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean(name = "securityManager")
public org.apache.shiro.mgt.SecurityManager securityManager(AuthRealm realm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setCacheManager(cacheManager());
// 关闭Shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
//使用RedisCacheManager需修改保存的redis的value
private RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setJedisPool(new JedisPool());
redisManager.setTimeout(-1);
redisManager.setPassword("");
return redisManager;
}
@Bean("shiroCacheManager")
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* 配置Shiro生命周期处理器
* @return
*/
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
}
5. 工具类
redisUtil 工具类(自行添加所需要用到的方法)
@Component
public class RedisUtil {
private static RedisTemplate<String, Object> redisTemplate;
//工具类是静态方法,用这样的方法将redisTemplate注入
//@PostConstruct 这个注解也可以,具体使用方法自行网上查询
@Autowired
public void setRedisTemplate(RedisTemplate redisTemplate) {
RedisUtil.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public static boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public static long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public static boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public static void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public static Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public static boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
TokenUtil工具类
public class TokenUtil {
//Token过期时间
private static final long EXPIRE_TIME = 10 * 60 * 1000;
private static final String TOKEN_SECRET = "shiro123";
//当前时间与过期时间差小于这个时间,token刷新
public static final long USED_TIME = 9 * 1000 * 60;
/**
* 生成签名
* 正常Token:Token未过期,且未达到建议更换时间。
* 濒死Token:Token未过期,已达到建议更换时间。
* 正常过期Token:Token已过期,但存在于缓存中。
* 非正常过期Token:Token已过期,不存在于缓存中
* @param **username**
* @param **password**
* @return String
*/
public static String sign(String username,Long userId) {
try {
// 设置过期时间
// 私钥和加密算法
// 设置头部信息
Map<String, Object> header = new HashMap<>(2);
header.put("Type", "Jwt");
header.put("alg", "HS256");
// 返回token字符串
return JWT.create()
.withHeader(header)
.withClaim("userName", username)
.withClaim("userId",userId)
//过期时间
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRE_TIME))
.sign(Algorithm.HMAC256(TOKEN_SECRET));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 检验是否更新token
*/
public static boolean verify(String token,String username,Long userId)throws JWTVerificationException {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("userName", username)
.withClaim("userId",userId)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
}
//获取token中的数据,不需要解密
public static <T> T getField(String token,String field,Class<T> clazz){
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(field).as(clazz);
}
}
ShiroConstant(shiro常量类)
public class ShiroConstant {
public static final String ROLE_SHIRO_CACHE = "role:userId:";
public static final String LOGIN_SHIRO_CACHE = "login:userId:";
}
6. controller控制层
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserRepository userRepository;
@Autowired
private PermissionRepsitory permissionRepsitory;
/**
* 登录
*/
@PostMapping("/login")
public Map<String, Object> login(String username, String password) {
Map<String, Object> result = new HashMap<>();
//用户信息
UserEntity user = userRepository.findFirstByUserName(username);
//账号不存在、密码错误
if (user == null) {
result.put("status", "400");
result.put("msg", "无该用户");
return result;
} else if (!user.getPassword().equals(password)) {
result.put("status", "400");
result.put("msg", "账号或密码有误");
return result;
} else {
//生成token,并保存到reids
String token = TokenUtil.sign(username,user.getId());
user.setToken(token);
RedisUtil.set(ShiroConstant.LOGIN_SHIRO_CACHE + user.getId(),user);
result.put("token",token);
result.put("status", "200");
result.put("msg", "登陆成功");
return result;
}
}
/**
* 退出
*/
@PostMapping("/logout")
public Map<String, Object> logout() {
Subject sub = SecurityUtils.getSubject();
log.info("user:" + sub.getPrincipal());
UserEntity user = (UserEntity)sub.getPrincipal();
RedisUtil.del(ShiroConstant.LOGIN_SHIRO_CACHE + user.getId(),ShiroConstant.ROLE_SHIRO_CACHE + user.getId());
Map<String, Object> result = new HashMap<>();
result.put("status", "200");
result.put("msg", "登出成功");
return result;
}
//保存用户
@PostMapping(value = "/save")
@RequiresPermissions({"1"})
public Map<String,String> saveUser(UserEntity user){
userRepository.save(user);
Map<String,String> result = new HashMap<>();
result.put("code","200");
result.put("msg","用户操作成功");
result.put("obj", JSONObject.toJSONString(user));
return result;
}
//删除用户
@PostMapping(value = "/del")
@RequiresPermissions({"2"})
public Map<String,String> deleteUser(Long userId){
Map<String,String> result = new HashMap<>();
Optional<UserEntity> o = userRepository.findById(userId);
if(o.isPresent()){
userRepository.deleteById(userId);
RedisUtil.del(ShiroConstant.ROLE_SHIRO_CACHE +userId,ShiroConstant.LOGIN_SHIRO_CACHE + userId);
result.put("code","200");
result.put("msg","用户删除成功");
}else{
result.put("code","400");
result.put("msg","没有这个用户");
}
return result;
}
//修改用户权限
@PostMapping(value = "/per")
@RequiresPermissions({"3"})
public Map<String,String> permission(PermissionEntity permissionEntity){
Map<String,String> result = new HashMap<>();
Optional<UserEntity> o = userRepository.findById(permissionEntity.getUserId());
if(o.isPresent()){
RedisUtil.del(ShiroConstant.ROLE_SHIRO_CACHE + permissionEntity.getUserId());
permissionRepsitory.save(permissionEntity);
result.put("code","200");
result.put("msg","权限添加成功");
}else{
result.put("code","400");
result.put("msg","没有这个用户");
}
return result;
}
}
发送请求 ---- 登陆
登陆成功并在redis保存了user的数据,将登陆后返回的token拿出来,在下一个请求中添加header中添加token
结果报错了!!!报错原因
解决办法:
在AuthRealm.java重写supports方法
//必须重写,不然会报错
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof AuthToken;
}
调用了保存用户
第一阶段的整合完成啦!!!