再spring boot中一次引入shiro、redis、token并将他们融合,参考了之前写好的一篇文章,对之前做的配置进行了简化shiro集成jwt - 问尤龙の时光 (wenyoulong.com)
pom文件的properties中添加版本信息
<shiro-spring-boot.version>1.13.0</shiro-spring-boot.version>
这里前端还没搭建,用thymeleaf做个简单的登录页
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>${shiro-spring-boot.version}</version>
</dependency>
<!-- thymeleaf模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
代码里的userService和roleSerivce是用于查询用户信息和用户角色的接口,换成自己的就可以了
package cn.com.wenyl.bs.config.shiro;
import cn.com.wenyl.bs.system.entity.SysRole;
import cn.com.wenyl.bs.system.entity.SysUser;
import cn.com.wenyl.bs.system.service.ISysRoleService;
import cn.com.wenyl.bs.system.service.ISysUserService;
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 javax.annotation.Resource;
import java.util.List;
/**
* @author Swimming Dragon
* @description: 配置认证和授权逻辑
* @date 2023年12月05日 9:59
*/
public class SysUserRealm extends AuthorizingRealm {
@Resource
private ISysUserService userService;
@Resource
private ISysRoleService roleService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String id = (String) principalCollection.getPrimaryPrincipal();
// 获取角色信息
List<SysRole> roleList = roleService.getUserRoleList(id);
//添加角色权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for(SysRole role:roleList){
simpleAuthorizationInfo.addRole(role.getRoleCode());
}
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
SysUser sysUer = userService.getByUserName(usernamePasswordToken.getUsername());
if(sysUer == null){
throw new AuthenticationException("账号"+usernamePasswordToken.getUsername()+"不存在");
}
if(String.copyValueOf(usernamePasswordToken.getPassword()).equals(sysUer.getPassword())){
return new SimpleAuthenticationInfo(sysUer,sysUer.getPassword(),getName());
}
throw new AuthenticationException("密码不正确");
}
}
下述代码中,我们开放了登录页面和登录接口,不需要认证就能访问,其他接口页面要认证通过才能访问
package cn.com.wenyl.bs.config;
import cn.com.wenyl.bs.config.shiro.SysUserRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.*;
/**
* @author Swimming Dragon
* @description: 配置shiro
* @date 2023年12月05日 9:55
*/
@Configuration
public class ShiroConfig {
@Bean
public SysUserRealm sysUserRealm() {
return new SysUserRealm();
}
/**
* 权限管理,配置主要是Realm的管理认证
* 需要使用redis存储认证信息,所以,关闭session,重写缓存管理器
* @return 安全管理器
*/
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(SysUserRealm sysUserRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
List<Realm> list = new ArrayList<>();
list.add(sysUserRealm);
securityManager.setRealms(list);
return securityManager;
}
/**
* Filter工厂,设置对应的过滤条件和跳转条件
* @param securityManager 安全管理器
* @return 过滤组件
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置过滤请求
Map<String,String> filterMap = new LinkedHashMap<>();
filterMap.put("/system/login/loginPage", "anon");
filterMap.put("/system/login/loginByUsernameAndPassword", "anon");
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
//未登录跳转地址
shiroFilterFactoryBean.setLoginUrl("/system/login/loginPage");
return shiroFilterFactoryBean;
}
}
package cn.com.wenyl.bs.system.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* @author Swimming Dragon
* @description: 登录接口
* @date 2023年12月05日 13:23
*/
@Slf4j
@Controller
@RequestMapping("/system/login")
@Api(tags="登录")
public class LoginController {
@GetMapping("/loginPage")
@ApiOperation(value="登录-跳转到登录页", notes="登录-跳转到登录页")
public String login(){
return "login";
}
@GetMapping("/indexPage")
@ApiOperation(value="登录-跳转到首页", notes="登录-跳转到首页")
public String index(){
return "index";
}
@PostMapping("/loginByUsernameAndPassword")
@ApiOperation(value="登录-跳转到登录页", notes="登录-跳转到登录页")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password) {
//1.获取Subject
Subject subject = SecurityUtils.getSubject();
//2.封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//3.执行登录方法
try {
subject.login(token);
//登录成功
//跳转到test.html
return "redirect:/system/login/indexPage";
} catch (UnknownAccountException e) {
//e.printStackTrace();
//登录失败:用户名不存在
System.out.println("用户名不存在");
return "login";
} catch (IncorrectCredentialsException e) {
//e.printStackTrace();
//登录失败:密码错误
System.out.println("密码错误");
return "login";
}
}
}
在resources的template目录下新建login.html和index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆页面</title>
</head>
<body>
<h3>成功</h3>
登录成功
</body>
</html>
登录页面访问了登录接口,替换成自己的登录接口地址即可
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆页面</title>
</head>
<body>
<h3>登录</h3>
<form method="post" action="/bs/system/login/loginByUsernameAndPassword">
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
启动项目后访问登录接口中的index页面,由于未登录,自己跳转到了login页面
输入用户名密码登录成功后,在访问index页面就成功了
shiro默认适用内存缓存,就是在用一个map把数据在内存中存储,详见org.apache.shiro.cache.MapCache,这个Cache只适用于单机环境(只有一个JVM),因此我们还是使用redis作为shiro的缓存
package cn.com.wenyl.bs.config.shiro;
/**
* @description: TODO
* @author Swimming Dragon
* @date 2023年12月06日 15:52
*/
public class ShiroConstant {
/**
* shir缓存过期时间(毫秒)
*/
public static final long SHIRO_CACHE_EXPIRE_TIME = 43200000;
}
package cn.com.wenyl.bs.config.shiro;
import cn.com.wenyl.bs.config.jwt.JwtConstant;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author Swimming Dragon
* @description: shiro实现redis缓存
* @date 2023年12月05日 16:50
*/
public class ShiroRedisCache implements Cache<String, Object>{
private final RedisTemplate<String,Object> redisTemplate;
private final String cachePrefix;
public ShiroRedisCache(RedisTemplate<String,Object> redisTemplate, String cachePrefix){
super();
this.redisTemplate = redisTemplate;
this.cachePrefix = cachePrefix;
}
@Override
public Object get(String k) throws CacheException {
return redisTemplate.opsForValue().get(cacheKey(k));
}
@Override
public Object put(String k, Object v) throws CacheException {
redisTemplate.opsForValue().set(k,v, ShiroConstant.SHIRO_CACHE_EXPIRE_TIME, TimeUnit.MICROSECONDS);
return v;
}
@Override
public Object remove(String k) throws CacheException {
Object v = this.get(k);
redisTemplate.opsForValue().getOperations().delete(k);
return v;
}
@Override
public void clear() throws CacheException {
throw new UnsupportedOperationException("不支持清理redis操作");
}
@Override
public int size() {
throw new UnsupportedOperationException("不支持获取redis键值对的大小操作");
}
@Override
public Set<String> keys() {
throw new UnsupportedOperationException("不支持获取redis所有key操作");
}
@Override
public Collection<Object> values() {
throw new UnsupportedOperationException("不支持获取redis所有value操作");
}
private String cacheKey(String key){
return cachePrefix + ":" + key;
}
}
package cn.com.wenyl.bs.config.shiro;
import lombok.Getter;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
/**
* @author Swimming Dragon
* @description: TODO
* @date 2023年12月05日 16:44
*/
public class ShiroRedisCacheManager implements CacheManager{
@Getter
private final RedisTemplate<String,Object> redisTemplate;
public ShiroRedisCacheManager(RedisTemplate<String,Object> redisTemplate){
this.redisTemplate = redisTemplate;
}
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public Cache getCache(String cachePrefix) throws CacheException {
return new ShiroRedisCache(getRedisTemplate(),cachePrefix);
}
}
在前面我们建立郭shiro配置类,现在在里面新增缓存部分配置
package cn.com.wenyl.bs.config;
import cn.com.wenyl.bs.config.shiro.ShiroRedisCacheManager;
import cn.com.wenyl.bs.config.shiro.SysUserRealm;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
import java.util.*;
/**
* @author Swimming Dragon
* @description: 配置shiro
* @date 2023年12月05日 9:55
*/
@Configuration
public class ShiroConfig {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Bean
public SysUserRealm sysUserRealm() {
return new SysUserRealm();
}
/**
* 权限管理,配置主要是Realm的管理认证
* 需要使用redis存储认证信息,所以,关闭session,重写缓存管理器
* @param sysUserRealm 用户认证管理
* @return 安全管理器
*/
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(SysUserRealm sysUserRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
List<Realm> list = new ArrayList<>();
list.add(sysUserRealm);
securityManager.setRealms(list);
// 关闭Shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
// 设置自定义Cache缓存
securityManager.setCacheManager(shiroRedisCacheManager());
return securityManager;
}
/**
* Filter工厂,设置对应的过滤条件和跳转条件
* @param securityManager 安全管理器
* @return 过滤组件
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置过滤请求
Map<String,String> filterMap = new LinkedHashMap<>();
filterMap.put("/system/login/loginPage", "anon");
filterMap.put("/system/login/loginByUsernameAndPassword", "anon");
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
//未登录跳转地址
shiroFilterFactoryBean.setLoginUrl("/system/login/loginPage");
return shiroFilterFactoryBean;
}
@Bean
public CacheManager shiroRedisCacheManager() {
return new ShiroRedisCacheManager(redisTemplate);
}
}
<jwt.version>3.3.0</jwt.version>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
package cn.com.wenyl.bs.config.jwt;
/**
* @author Swimming Dragon
* @description: 公钥私钥等信息
* @date 2023年12月06日 13:59
*/
public class JwtConstant {
/**
* token过期时间(毫秒)
*/
public static final long ACCESS_TOKEN_EXPIRE_TIME = 43200000;
/**
* 用户登录账号属性名
*/
public static final String ACCOUNT_PROPERTY_NAME = "username";
/**
* token在request header中的属性名
*/
public static final String REQUEST_HEADER_TOKEN = "BS-WEB-TOKEN";
/**
* shiro的token在redis中的缓存key的前缀
*/
public static final String PREFIX_SHIRO_WEB_TOKEN = "shiro:web_token:";
/**
* token不存在或者无效的返回信息
*/
public static final String TOKEN_IS_INVALID_MSG = "请先登录";
}
package cn.com.wenyl.bs.config.jwt;
import cn.com.wenyl.bs.utils.R;
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 com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
/**
* @author Swimming Dragon
* @description: jwt工具类
* @date 2023年12月06日 13:24
*/
public class JwtUtil {
/**
* 构建一个token
* @param username 用户名
* @param password 用户密码
* @return 返回token
*/
public static String createToken(String username,String password) throws UnsupportedEncodingException {
Date date = new Date(System.currentTimeMillis() + JwtConstant.ACCESS_TOKEN_EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(password);
return JWT.create().withClaim(JwtConstant.ACCOUNT_PROPERTY_NAME, username).withExpiresAt(date).sign(algorithm);
}
/**
* 校验token
* @param token token
* @param username 用户名
* @param password 用户密码
* @return 返回token是否合法
*/
public static boolean verifyToken(String token, String username, String password) {
try {
// 根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(password);
JWTVerifier verifier = JWT.require(algorithm).withClaim(JwtConstant.ACCOUNT_PROPERTY_NAME, username).build();
// 效验TOKEN
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 根据token获取用户名
* @param token token
* @return 用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(JwtConstant.ACCOUNT_PROPERTY_NAME).asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* response 响应错误
* @param response 响应
* @param code 错误编码
* @param errorMsg 错误信息
*/
public static void responseError(ServletResponse response, Integer code, String errorMsg) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// issues/I4YH95浏览器显示乱码问题
httpServletResponse.setHeader("Content-type", "text/html;charset=UTF-8");
R ret = R.error(code,errorMsg);
OutputStream os;
try {
os = httpServletResponse.getOutputStream();
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setStatus(code);
os.write(new ObjectMapper().writeValueAsString(ret).getBytes(StandardCharsets.UTF_8));
os.flush();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
package cn.com.wenyl.bs.config.jwt;
import lombok.Data;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @author Swimming Dragon
* @description: jwt token
* @date 2023年12月06日 14:13
*/
@Data
public class JwtToken implements AuthenticationToken {
/**
* Token
*/
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
package cn.com.wenyl.bs.config.jwt;
import cn.com.wenyl.bs.system.entity.SysRole;
import cn.com.wenyl.bs.system.entity.SysUser;
import cn.com.wenyl.bs.system.service.ISysRoleService;
import cn.com.wenyl.bs.system.service.ISysUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
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.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
import java.util.List;
/**
* @author Swimming Dragon
* @description: TODO
* @date 2023年12月06日 14:15
*/
@Slf4j
public class JwtRealm extends AuthorizingRealm {
@Resource
private ISysUserService userService;
@Resource
private ISysRoleService roleService;
@Resource
private RedisTemplate<String,Object> redisTemplate;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String id = (String) principals.getPrimaryPrincipal();
// 获取角色信息
List<SysRole> roleList = roleService.getUserRoleList(id);
//添加角色权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for(SysRole role:roleList){
simpleAuthorizationInfo.addRole(role.getRoleCode());
}
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();
// 解密获得account,用于和数据库进行对比
String username = JwtUtil.getUsername(token);
// 帐号为空
if (StringUtils.isBlank(username)) {
log.error("Token中帐号为空");
throw new AuthenticationException("Token中帐号为空");
}
// 查询用户是否存在
SysUser sysUser = userService.getByUserName(username);
if (sysUser == null) {
throw new AuthenticationException("帐号"+username+"不存在");
}
// 验证token和refreshToken
Boolean exists = redisTemplate.hasKey(JwtConstant.PREFIX_SHIRO_WEB_TOKEN + username);
if (JwtUtil.verifyToken(token,username,sysUser.getPassword()) && exists != null && exists) {
return new SimpleAuthenticationInfo(sysUser, token, getName());
}
throw new AuthenticationException("Token已过期)");
}
/**
* 指定当前realm适用的token类型为JwtToken
* @param authenticationToken the token being submitted for authentication. 认证的token
* @return 如果类型为JwtToken则返回true,否则返回false
*/
@Override
public boolean supports(AuthenticationToken authenticationToken) {
return authenticationToken instanceof JwtToken;
}
}
package cn.com.wenyl.bs.config.jwt;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author Swimming Dragon
* @description: jwt过滤器,用来做token校验
* @date 2023年12月06日 13:10
*/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter{
public JwtFilter(){}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
JwtUtil.responseError(response, HttpStatus.SC_UNAUTHORIZED,JwtConstant.TOKEN_IS_INVALID_MSG);
return false;
}
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(JwtConstant.REQUEST_HEADER_TOKEN);
JwtToken jwtToken = new JwtToken(token);
getSubject(request, response).login(jwtToken);
return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
return super.preHandle(request, response);
}
}
修改为如下配置
package cn.com.wenyl.bs.config;
import cn.com.wenyl.bs.config.jwt.JwtFilter;
import cn.com.wenyl.bs.config.jwt.JwtRealm;
import cn.com.wenyl.bs.config.shiro.ShiroRedisCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.*;
/**
* @author Swimming Dragon
* @description: 配置shiro
* @date 2023年12月05日 9:55
*/
@Configuration
public class ShiroConfig {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Bean
public JwtRealm getJwtRealm() {
return new JwtRealm();
}
/**
* 权限管理,配置主要是Realm的管理认证
* 需要使用redis存储认证信息,所以,关闭session,重写缓存管理器
* @param jwtRealm jwt认证管理
* @return 安全管理器
*/
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
List<Realm> list = new ArrayList<>();
list.add(jwtRealm);
securityManager.setRealms(list);
// 关闭Shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
// 设置自定义Cache缓存
securityManager.setCacheManager(shiroRedisCacheManager());
return securityManager;
}
/**
* Filter工厂,设置对应的过滤条件和跳转条件
* @param securityManager 安全管理器
* @return 过滤组件
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置过滤器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwtFilter",new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//设置请求规则,登录页面放行,其他页面需要认证
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/system/login/loginPage", "anon");
filterChainDefinitionMap.put("/system/login/loginByUsernameAndPassword", "anon");
filterChainDefinitionMap.put("/**", "jwtFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//未登录跳转地址
shiroFilterFactoryBean.setLoginUrl("/system/login/loginPage");
return shiroFilterFactoryBean;
}
@Bean
public CacheManager shiroRedisCacheManager() {
return new ShiroRedisCacheManager(redisTemplate);
}
}
package cn.com.wenyl.bs.system.controller;
import cn.com.wenyl.bs.config.jwt.JwtConstant;
import cn.com.wenyl.bs.config.jwt.JwtUtil;
import cn.com.wenyl.bs.system.entity.SysUser;
import cn.com.wenyl.bs.system.service.ISysUserService;
import cn.com.wenyl.bs.utils.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.TimeUnit;
/**
* @author Swimming Dragon
* @description: 登录接口
* @date 2023年12月05日 13:23
*/
@Slf4j
@Controller
@RequestMapping("/system/login")
@Api(tags="登录")
public class LoginController {
@Resource
private ISysUserService sysUserService;
@Resource
private RedisTemplate<String,Object> redisTemplate;
@GetMapping("/loginPage")
@ApiOperation(value="登录-跳转到登录页", notes="登录-跳转到登录页")
public String login(){
return "login";
}
@GetMapping("/indexPage")
@ApiOperation(value="登录-跳转到首页", notes="登录-跳转到首页")
public String index(){
return "index";
}
@PostMapping("/loginByUsernameAndPassword")
@ApiOperation(value="登录-跳转到登录页", notes="登录-跳转到登录页")
@ResponseBody
public R<?> login(@RequestParam("username") String username,
@RequestParam("password") String password) {
Boolean exist = redisTemplate.hasKey(JwtConstant.PREFIX_SHIRO_WEB_TOKEN+username);
if(exist != null && exist){
return R.ok("已登录,不要重复登录");
}
// 获取用户
SysUser sysUser = sysUserService.getByUserName(username);
if(!password.equals(sysUser.getPassword())){
return R.error("密码错误,回到登录页");
}
// 获取token
String token;
try {
token = JwtUtil.createToken(username, password);
} catch (UnsupportedEncodingException e) {
log.error(e.getMessage());
return R.error(e.getMessage());
}
redisTemplate.opsForValue().set(JwtConstant.PREFIX_SHIRO_WEB_TOKEN+username,token, JwtConstant.ACCESS_TOKEN_EXPIRE_TIME, TimeUnit.MICROSECONDS);
return R.ok(token);
}
@RequestMapping(value = "/logout")
public R<?> logout(HttpServletRequest request) {
//用户退出逻辑
String token = request.getHeader(JwtConstant.REQUEST_HEADER_TOKEN);
if(StringUtils.isEmpty(token)) {
return R.error("退出登录失败!");
}
String username = JwtUtil.getUsername(token);
SysUser sysUser = sysUserService.getByUserName(username);
if(sysUser!=null) {
redisTemplate.opsForValue().getOperations().delete(JwtConstant.PREFIX_SHIRO_WEB_TOKEN+username);
//调用shiro的logout
SecurityUtils.getSubject().logout();
log.info(" 用户名: "+username+",退出成功! ");
return R.ok("退出登录成功!");
}else {
log.info(" 用户名: "+username+"退出登录失败,用户不存在!");
return R.error("退出登录失败,用户不存在!");
}
}
}
访问首页,提示401
进入登录页,登录成功后,返回token