前言
思路分析
核心代码分析
一、依赖引入和数据库表
二、注册
三、密码登录
四、携带jwt访问
五、Shiro的自定义配置类
花了几天了解Shiro框架(也不算太深入),根据网上资料做了一个Demo:SpringBoot 2.2.9.RELEASE+ Shiro + Jwt 实现登录认证。
1、这个Demo关注 登录授权(比较通用),基本不涉及权限控制,因为权限控制设计到具体业务。所以可以本Demo可以根据自身实际情况稍加修改,就可以作为前后端分离项目的登录模块。
2、本博客适用于对Shiro和Jwt有了解,起码对于几个关键的类的作用及方法有了解,下面我不会展开,只会对Demo的思路和代码做分析。
1、Shiro原本的登录认证的流程
subject.login(new UsernamePasswordToken(username, password)); 这是执行登录认证的关键代码,然后Shiro内部会在对应的Realm里面的doGetAuthenticationInfo()中根据username从数据库中查找正确的User信息(包括密码信息,密码可以被加密存储)。然后将正确的User信息封装成AuthenticationInfo对象。在比较器(CredentialsMatcher)的doCredentialsMatch()方法中按照一定规则进行密码匹配(比较)。如果匹配成功,就登录成功。
登录成功后,subject的登录信息会被存储session中。那么下次访问受限的资源或接口,便不需要再登陆了。
2、加入Jwt后的登录认证的流程
Jwt和Shiro原本的登录认证有冲突吗? 其实Jwt本质上就是一种特殊的token而已。换言之,Shiro + Jwt的意思就是使用token(Jwt)替换到Shiro原本的session,使用token的服务端更加适用于前后端分离的项目。不管前端是vue项目还是Android APP等等。
替换之后的流程是什么样的? 首先,第一次登录,我们需要用户输入username和password(根据实际情况,此处简单举例),然后按照Shiro原本的登录认证流程登录。如果登录成功,服务端返回一个Jwt字符串(相当于签发一个令牌)给前端。下次用户访问服务端受限的资源,只要携带正确合法的Jwt就可以访问了。
实现的主要思路是怎样的?我们需要实现一个过滤器,拦截所有请求,对于请求头中含有Jwt的请求进行特殊处理,我们不使用Shiro默认登录流程来处理,而是使用我们自定义的处理流程,包括实现对应的Token,Realm,CredentialsMatcher。当认证通过之后,将请求放行到Controller层。
经过一顿分析,本Demo主要划分3个部分:注册、密码登录、携带令牌(jwt)访问。其中,“携带令牌(jwt)访问”这个部分就是Shiro整合Jwt的核心逻辑。
数据库相关等其他依赖我就不贴出来了,详情参照Demo的Github源码:https://github.com/passerbyYSQ/SpringBoot_Shiro_Jwt
【推荐】Shiro的项目实战使用参考(这个项目打磨了比较久,Shiro的使用可参考该项目):
GitHub - passerbyYSQ/forum: Forum(社区&论坛),分布式项目。由于毕设需要,正在逐渐集成即时通讯功能(视频弹幕和在线聊天)。希望各位大佬可以一起贡献代码,一起完善这个凝聚了我很多心血和时间的开源作品!
org.apache.shiro
shiro-spring-boot-starter
1.5.3
org.apache.shiro
shiro-ehcache
1.5.3
com.auth0
java-jwt
3.3.0
注册的时候,我们需要将用户输入的明文密码通过Md5Hash加密处理,传入加密的随机盐(每个用户不一样,需要存到用户表)和哈希散列的次数(每个用户都一样)。
那么在使用密码登录时,我们需要以相同的加密规则加密用户输入的密码,然后与数据库中存储的注册时的加密密码,进行比对。如果一样,登录成功。
UserController
/**
* 通过注解实现权限控制
* 在方法前添加注解 @RequiredRoles 或 @RequiredPermissions
*
* @author passerbyYSQ
* @create 2020-08-20 23:54
*/
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 用户注册
* @param user
* @return
*/
@PostMapping("/register")
public ResponseEntity register(User user) {
// 参数判断省略
// ...
try {
userService.register(user);
return ResponseEntity.ok().build();
} catch (Exception e) {
e.printStackTrace();
}
// 错误提示信息省略...
return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body("客户端传参错误");
}
}
UserServiceImpl
/**
* @author passerbyYSQ
* @create 2020-08-21 11:02
*/
@Service("userService")
@Transactional // 开启事务。有需要再开启
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Override
public void register(User user) {
// 8个字符的随机字符串,作为加密的随机盐
String salt = RandomUtil.generateStr(8);
// 需要保存到数据库,第一次登录(认证)比较时需要使用
user.setSalt(salt);
// Md5Hash默认将随机盐拼接到源字符串的前面,然后使用md5加密,再经过x次的哈希散列
// 第三个参数(hashIterations):哈希散列的次数
Md5Hash md5Hash = new Md5Hash(user.getPassword(), user.getSalt(), 1024);
user.setPassword(md5Hash.toHex());
// 保存
userDAO.save(user);
}
}
UserController
/**
* 通过注解实现权限控制
* 在方法前添加注解 @RequiredRoles 或 @RequiredPermissions
*
*
* @author passerbyYSQ
* @create 2020-08-20 23:54
*/
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 用户登录(身份认证)
* Shiro会缓存认证信息
*
* @param username
* @param password
* @return
*/
@PostMapping("/login")
public ResponseEntity login(String username, String password) {
// 前期的注入工作已经由SpringBoot完成了
// 获取当前来访用户的主体对象
Subject subject = SecurityUtils.getSubject();
try {
// 执行登录,如果登录失败会直接抛出异常,并进入对应的catch
subject.login(new UsernamePasswordToken(username, password));
// 获取主体的身份信息
// 实际上是User。为什么?
// 取决于LoginRealm中的doGetAuthenticationInfo()方法中SimpleAuthenticationInfo构造函数的第一个参数
User user = (User) subject.getPrincipal();
// 生成jwt
String jwt = userService.generateJwt(user.getUsername());
// 将jwt放入到响应头中
return ResponseEntity.ok().header("token", jwt).build();
} catch (UnknownAccountException e) {
// username 错误
e.printStackTrace();
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("username不存在");
} catch (IncorrectCredentialsException e) {
// password 错误
e.printStackTrace();
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("password错误");
}
}
/**
* 退出登录
* 销毁主体的认证记录(信息),下次访问需要重新认证
*
* @return
*/
@RequestMapping("/logout")
public ResponseEntity logout() {
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
userService.logout(user.getUsername());
subject.logout();
return ResponseEntity.ok().build();
}
}
UserServiceImpl
/**
* @author passerbyYSQ
* @create 2020-08-21 11:02
*/
@Service("userService") // 不要忘了
@Transactional // 开启事务。有需要再开启
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Override
public String generateJwt(String username) {
// 8个字符的随机字符串,作为生成jwt的随机盐
// 保证每次登录成功返回的Token都不一样
String jwtSecret = RandomUtil.generateStr(8);
// 将此次登录成功的jwt secret存到数据库,下次携带jwt时解密需要使用
userDAO.updateJwtSecretByUsername(username, jwtSecret);
return JwtUtil.generateJwt(username, jwtSecret);
}
@Override
public User findByUsername(String username) {
return userDAO.findByUsername(username);
}
@Override
public void logout(String username) {
// 将jwt secret置为空
userDAO.updateJwtSecretByUsername(username, "");
}
}
LoginRealm:处理密码登录的Realm,复写doGetAuthenticationInfo()方法
/**
* @author passerbyYSQ
* @create 2020-08-20 23:31
*/
public class LoginRealm extends AuthorizingRealm {
/*
如果@Autowired,需要在当前类前加上@Componet注解,将当前类的实例注入到IOC容器
但是如果有多个类似的类,都要注册到容器中,不太好。我可以新建一个管理类,注册到容器中,
为我们统一获取@Autowired的实例
*/
// @Autowired
// private UserService userService;
/**
* 或者在ShiroConfig中设置
*/
public LoginRealm() {
// 匹配器。需要与密码加密规则一致
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 设置匹配器的加密算法
hashedCredentialsMatcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
// 设置匹配器的哈希散列次数
hashedCredentialsMatcher.setHashIterations(1024);
// 将对应的匹配器设置到Realm中
this.setCredentialsMatcher(hashedCredentialsMatcher);
}
/**
* 可以往Shiro中注册多种Realm。某种Token对象需要对应的Realm来处理。
* 复写该方法表示该方法支持处理哪一种Token
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 从Token中获取身份信息。这里实际上是username,这里从UsernamePasswordToken的源码可以看出
String principal = (String) token.getPrincipal();
// 从IOC容器中获取UserService组件
UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
User user = userService.findByUsername(principal);
if (!ObjectUtils.isEmpty(user)) {
// 返回正确的信息(数据库存储的),作为比较的基准
return new SimpleAuthenticationInfo(
user, user.getPassword(),
ByteSource.Util.bytes(user.getSalt()), this.getName()
);
}
return null;
}
}
密码登录的主要流程基本就这么多。但是还需要写配置类将LoginRealm注册到Shiro中,这个后面统一把 Shiro 的配置类贴出来。上面代码还涉及到两个工具类:
ApplicationContextUtil:用于更加灵活地获取IOC容器中组件
JwtUtil:jwt的工具类,提供几个主要的静态方法,包括:生成jwt,校验jwt,获取jwt里面的数据,获取jwt的签发时间。
ApplicationContextUtil
/**
* @author passerbyYSQ
* @create 2020-08-21 11:50
*/
@Component // 加入容器
public class ApplicationContextUtil implements ApplicationContextAware {
// IOC容器
private static ApplicationContext context;
/**
* 将IOC容器回调给我们,我们将它缓存起来
*
* @param applicationContext
* @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
/**
* 从IOC容器中获取组件(bean)
*
* @param beanName
* @return
*/
public static Object getBean(String beanName) {
return context.getBean(beanName);
}
}
JwtUtil
/**
* JWT的工具类,包括签发、验证、获取信息
*
* @author passerbyYSQ
* @create 2020-08-22 11:13
*/
public class JwtUtil {
// 有效时间:7天
private static final long EFFECTIVE_DURATION = 1000 * 60 * 60 * 24 * 7;
// 发行者
private static final String ISSUER = "net.ysq";
/**
* 生成Jwt字符串
*
* @param claims 由于类库只支持基本类型的包装类、String、Date,我们最好使用String
* @param secret 加密的密钥
* @return
*/
public static String generateJwt(Map claims, String secret) {
// 发行时间
Date issueAt = new Date();
// 过期时间
Date expireAt = new Date(issueAt.getTime() + EFFECTIVE_DURATION);
// 加密算法
Algorithm algorithm = Algorithm.HMAC256(secret.getBytes(StandardCharsets.UTF_8));
JWTCreator.Builder builder = JWT.create()
.withIssuer(ISSUER)
.withIssuedAt(issueAt)
.withExpiresAt(expireAt);
// 设置Payload信息
Set keySet = claims.keySet();
for (String key : keySet) {
builder.withClaim(key, claims.get(key));
}
return builder.sign(algorithm);
}
public static String generateJwt(String username, String secret) {
Map claims = new HashMap<>();
claims.put("username", username);
return generateJwt(claims, secret);
}
/**
* 校验jwt是否合法
*
* @param jwt
* @param claims
* @return
*/
public static boolean verifyJwt(String jwt, Map claims, String secret) {
// 解密算法
Algorithm algorithm = Algorithm.HMAC256(secret.getBytes(StandardCharsets.UTF_8));
try {
Verification verification = JWT.require(algorithm).withIssuer(ISSUER);
Set keySet = claims.keySet();
for (String key : keySet) {
verification.withClaim(key, claims.get(key));
}
JWTVerifier verifier = verification.build();
verifier.verify(jwt);
return true;
} catch (IllegalArgumentException | JWTVerificationException e) {
e.printStackTrace();
}
return false;
}
public static boolean verifyJwt(String jwt, String username, String secret) {
Map claims = new HashMap<>();
claims.put("username", username);
return verifyJwt(jwt, claims, secret);
}
/**
* 根据key获取claim值
*
* @param jwt
* @param key
* @return
*/
public static String getClaimByKey(String jwt, String key) {
try {
DecodedJWT decodedJwt = JWT.decode(jwt);
return decodedJwt.getClaim(key).asString(); // 注意不要用toString
} catch (JWTDecodeException e) {
e.printStackTrace();
}
return null;
}
/**
* 返回过期的时间
*
* @param jwt
* @return
*/
public static Date getExpireAt(String jwt) {
try {
DecodedJWT decodedJwt = JWT.decode(jwt);
return decodedJwt.getExpiresAt();
} catch (JWTDecodeException e) {
e.printStackTrace();
}
return null;
}
}
JwtToken:类比UsernamePasswordToken
/**
* 或者直接实现AuthenticationToken也可以,不需要host
*
* @author passerbyYSQ
* @create 2020-08-22 10:42
*/
public class JwtToken implements HostAuthenticationToken {
// JWT字符串
private String token;
private String host;
public JwtToken(String token) {
this(token, null);
}
public JwtToken(String token, String host) {
this.token = token;
this.host = host;
}
@Override
public String getHost() {
return token;
}
/**
* 返回身份信息(相当于username),这个方法的返回比较重要,前面的代码也说到了
* Jwt里面包含一个访问主体的身份(比如说username)
* @return
*/
@Override
public Object getPrincipal() {
return token;
}
/**
* 返回凭证信息(相当于password)
* Jwt本身就是一个令牌凭证,在服务端通过解密校验
* @return
*/
@Override
public Object getCredentials() {
return token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public void setHost(String host) {
this.host = host;
}
}
JwtAuthenticatingFilter:全局请求的过滤器。驳回没有携带token的请求,不能访问受限资源。对于携带token的请求,校验token是否有效,有效最重放行到controller层
/**
* @author passerbyYSQ
* @create 2020-08-22 12:06
*/
public class JwtAuthenticatingFilter extends BasicHttpAuthenticationFilter {
// 是否刷新token
private boolean shouldRefreshToken;
public JwtAuthenticatingFilter() {
this.shouldRefreshToken = false;
}
/**
* 请求是否允许放行
* 父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。这里在不通过时,还调用了isPermissive()方法,我们后面解释。
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
boolean allowed = false;
try {
allowed = executeLogin(request, response);
} catch(IllegalStateException e){ //not found any token
System.out.println("Not found any token");
}catch (Exception e) {
System.out.println("Error occurs when login");
}
return allowed || super.isPermissive(mappedValue);
}
/**
* 父类executeLogin()首先会createToken(),然后调用shiro的Subject.login()方法。
*
* executeLogin()的逻辑是不是跟UserController里面的密码登录逻辑很像?
*
* @param request
* @param response
* @return
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 从请求头中的Authorization字段尝试获取jwt token
String token = httpRequest.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
// 从请求头中的token字段(自定义字段)尝试获取jwt token
token = httpRequest.getHeader("token");
}
if (StringUtils.isEmpty(token)) {
// 从url参数中尝试获取jwt token
token = httpRequest.getParameter("token");
}
if (!StringUtils.isEmpty(token)) {
return new JwtToken(token);
}
return null;
}
/**
* 如果这个Filter在之前isAccessAllowed()方法中返回false,则会进入这个方法。我们这里直接返回错误的response
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = WebUtils.toHttp(response);
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.setStatus(HttpStatus.NON_AUTHORITATIVE_INFORMATION.value());
PrintWriter writer = response.getWriter();
writer.print("无效token");
fillCorsHeader(request, httpResponse);
return false;
}
/**
* 登录成功后判断是否需要刷新token
* 登录成功说明:jwt有效,尚未过期。当离过期时间不足一天时,往响应头中放入新的token返回给前端
*
* @param token
* @param subject
* @param request
* @param response
* @return
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
ServletRequest request, ServletResponse response) {
String oldToken = (String) token.getPrincipal();
Date expireAt = JwtUtil.getExpireAt(oldToken);
int countDownDays = (int) DateTimeUtil.differDaysBetween(
LocalDateTime.now(), DateTimeUtil.toLocalDateTime(expireAt));
if (shouldRefreshToken && !ObjectUtils.isEmpty(expireAt)
&& countDownDays < 1) { // 如果离过期时间不足一天
UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
User user = (User) subject.getPrincipal();
String newToken = userService.generateJwt(user.getUsername());
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.addHeader("token", newToken);
}
return true;
}
/**
* 添加跨域支持
* @param request
* @param response
* @throws Exception
*/
@Override
protected void postHandle(ServletRequest request, ServletResponse response) {
fillCorsHeader(request, response);
}
/**
* 设置跨域
*/
public void fillCorsHeader(ServletRequest request, ServletResponse response) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-control-Allow-Origin", httpRequest.getHeader("Origin"));
httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpResponse.setStatus(HttpStatus.OK.value());
}
}
public boolean isShouldRefreshToken() {
return shouldRefreshToken;
}
public void setShouldRefreshToken(boolean shouldRefreshToken) {
this.shouldRefreshToken = shouldRefreshToken;
}
}
JwtRealm
/**
* @author passerbyYSQ
* @create 2020-08-23 18:24
*/
public class JwtRealm extends AuthorizingRealm {
public JwtRealm() {
// 用我们自定的Matcher
this.setCredentialsMatcher(new JwtCredentialsMatcher());
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// JwtToken jwtToken = (JwtToken) token;
// String tokenStr = jwtToken.getToken();
// 取决于JwtToken的getPrincipal()
String tokenStr = (String) token.getPrincipal();
// 从jwt字符串中解析出username信息
String username = JwtUtil.getClaimByKey(tokenStr, "username");
if (!Strings.isEmpty(username)) {
UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
// 根据token中的username去数据库核对信息,返回用户信息,并封装称SimpleAuthenticationInfo给Matcher去校验
User user = userService.findByUsername(username);
// principle是身份信息,简单的可以放username,也可以将User对象作为身份信息
// 身份信息可以在登录成功之后通过subject.getPrinciple()取出
return new SimpleAuthenticationInfo(user, user.getJwtSecret(), this.getName());
}
return null;
}
}
JwtCredentialsMatcher:Jwt的比较器
/**
* @author passerbyYSQ
* @create 2020-08-23 18:42
*/
public class JwtCredentialsMatcher implements CredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
// AuthenticationInfo info 是我们在JwtRealm中doGetAuthenticationInfo()返回的那个
User user = (User) info.getPrincipals().getPrimaryPrincipal();
String secret = (String) info.getCredentials();
// String tokenStr = ((JwtToken) token).getToken();
String tokenStr = (String) token.getPrincipal();
// 校验jwt有效
return JwtUtil.verifyJwt(tokenStr, user.getUsername(), secret);
}
}
总结来说,这个配置类主要干两种事情:
1、将我们上述定义的Realm,过滤器等,注册给Shiro
2、设置请求的拦截规则
代码解析细节,参看注释。个人强迫症,注释写的还算是比较可以的。
/**
* 整合Shiro框架的配置类
*
* @author passerbyYSQ
* @create 2020-08-20 23:10
*/
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(
DefaultWebSecurityManager securityManager, ShiroFilterChainDefinition chainDefinition) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 必须的设置。我们自定义的Realm此时已经被设置到securityManager中了
factoryBean.setSecurityManager(securityManager);
// 注册我们写的过滤器
Map filters = factoryBean.getFilters();
filters.put("jwtAuth", new JwtAuthenticatingFilter());
factoryBean.setFilters(filters);
// 设置请求的过滤规则。其中过滤规则中用到了我们注册的过滤器:jwtAuth
factoryBean.setFilterChainDefinitionMap(chainDefinition.getFilterChainMap());
return factoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager(Authenticator authenticator) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 所有的Realm都用这个全局缓存。不生效,需要在realm中设置缓存。原因暂时搞不懂。
// securityManager.setCacheManager(new EhCacheManager());
securityManager.setAuthenticator(authenticator);
return securityManager;
}
/**
* 设置请求的过滤规则
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/user/register", "noSessionCreation,anon");
chainDefinition.addPathDefinition("/user/login", "noSessionCreation,anon"); //login不做认证,noSessionCreation的作用是用户在操作session时会抛异常
// 注意第2个参数的"jwtAuth"需要与上面的 filters.put("jwtAuth", new JwtAuthenticatingFilter()); 一致
chainDefinition.addPathDefinition("/user/logout", "noSessionCreation,jwtAuth[permissive]"); //做用户认证,permissive参数的作用是当token无效时也允许请求访问,不会返回鉴权未通过的错误
chainDefinition.addPathDefinition("/**", "noSessionCreation,jwtAuth"); // 默认进行用户鉴权
return chainDefinition;
}
/**
* 初始化Authenticator,将我们需要的Realm设置进去
* Shiro会将Authenticator设置到SecurityManager里面
*/
@Bean
public Authenticator authenticator(@Qualifier("loginRealm") Realm loginRealm, @Qualifier("jwtRealm") Realm jwtRealm) {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
//设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证
authenticator.setRealms(Arrays.asList(loginRealm, jwtRealm));
//设置多个realm认证策略,一个成功即跳过其它的
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
return authenticator;
}
/**
* 返回我们自定义的Realm
*
* @return
*/
@Bean("loginRealm") // 自动配置类中有同名组件,如果只写@Bean,会出现歧义
public Realm loginRealm(EhCacheManager ehCacheManager) {
LoginRealm loginRealm = new LoginRealm();
// AuthenticatingRealm里面的isAuthenticationCachingEnabled()
loginRealm.setCacheManager(ehCacheManager);
loginRealm.setCachingEnabled(true); // 这句话不能少!!!
loginRealm.setAuthenticationCachingEnabled(true); // 认证缓存
loginRealm.setAuthorizationCachingEnabled(true); // 授权缓存
return loginRealm;
}
@Bean("jwtRealm")
public Realm jwtRealm(EhCacheManager ehCacheManager) {
JwtRealm jwtRealm = new JwtRealm();
jwtRealm.setCacheManager(ehCacheManager);
jwtRealm.setCachingEnabled(true); // 这句话不能少!!!
jwtRealm.setAuthenticationCachingEnabled(true); // 认证缓存
jwtRealm.setAuthorizationCachingEnabled(true); // 授权缓存
return jwtRealm;
}
/**
* 禁用session, 不保存用户登录状态。保证每次请求都重新认证。
* 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session,
* 如果要完全禁用,要配合上过滤规则的noSessionCreation的Filter来实现
*/
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator(){
DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
/**
* shiro的全局缓存管理器
* @return
*/
@Bean
public EhCacheManager ehCacheManager() {
return new EhCacheManager();
}
}
博客注重思路逻辑分析。有需要的同学可以去Github自取源码。GitHub - passerbyYSQ/SpringBoot_Shiro_Jwt
求赞和收藏。。。么么哒