功能:登录成功时返回token,之后的请求携带token进行认证授权;之前是认证一次,由session存储用户信息,现在是使用jwt(无状态)每次请求都需要重新认证
1 github:https://github.com/yezhimincxvxb/shiro/tree/master/shiro09
2 shiro09 子工程
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yzm</groupId>
<artifactId>shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>shiro09</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>shiro09</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>com.yzm</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目结构
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test3?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
type-aliases-package: com.yzm.shiro09.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3 自定义JwtToken
之前是通过用户名和密码进行认证,使用的是UsernamePasswordToken
现在第一次登录认证还是用户名和密码,之后的请求进行认证就使用我们自定义的JwtToken
JwtToken.java
package com.yzm.shiro09.entity;
import org.apache.shiro.authc.AuthenticationToken;
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = -4763390136373610135L;
private String token;
public JwtToken(String token) {
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
Shiro通过supports()来决定使用哪块Realm进行认证
/**
* 限定这个Realm只支持我们自定义的JWT Token
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
4 自定义拦截器JwtFilter,指定哪些url需要携带token并生成JwtToken
isAccessAllowed() 该拦截器的入口
createToken() 获取请求头的token并判断是否过期
JwtFilter.java
package com.yzm.shiro09.config;
import com.alibaba.fastjson.JSONObject;
import com.yzm.common.entity.HttpResult;
import com.yzm.shiro09.entity.JwtToken;
import com.yzm.shiro09.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class JwtFilter extends AuthenticatingFilter {
/**
* 跨域支持
* 前置处理
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 跨域支持
* 后置处理
*/
@Override
protected void postHandle(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
}
/**
* 父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
boolean allowed = false;
try {
allowed = executeLogin(request, response);
} catch (IllegalStateException e) {
log.error("Not found any token");
} catch (Exception e) {
log.error("Error occurs when login", e);
}
return allowed || super.isPermissive(mappedValue);
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = this.createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
} else {
try {
Subject subject = this.getSubject(request, response);
subject.login(token);
return this.onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException var5) {
return this.onLoginFailure(token, var5, request, response);
}
}
}
/**
* 这里重写了父类的方法,使用我们自己定义的Token类,提交给shiro。这个方法返回null的话会直接抛出异常,进入isAccessAllowed()的异常处理逻辑。
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
String jwtToken = JwtUtils.getTokenFromRequest(WebUtils.toHttp(request));
if (StringUtils.isNotBlank(jwtToken)) return new JwtToken(jwtToken);
return null;
}
/**
* 如果这个Filter在之前isAccessAllowed()方法中返回false,则会进入这个方法。我们这里直接返回错误的response
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
HttpResult result = new HttpResult(500, "请求失败", null);
response.getWriter().print(JSONObject.toJSONString(result, true));
response.getWriter().flush();
response.getWriter().close();
return false;
}
/**
* 如果Shiro Login认证成功,会进入该方法,等同于用户名密码登录成功,我们这里还判断了是否要刷新Token
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
log.info("token认证成功");
if (token instanceof JwtToken) {
JwtToken jwtToken = (JwtToken) token;
String tokenStr = jwtToken.getToken();
if (JwtUtils.isExpired(tokenStr)) {
log.info("刷新token");
String username = (String) subject.getPrincipal();
Map<String, Object> map = new HashMap<>();
map.put(JwtUtils.USERNAME, username);
tokenStr = JwtUtils.generateToken(map);
}
HttpServletResponse httpResponse = WebUtils.toHttp(response);
httpResponse.setHeader("token", tokenStr);
}
return true;
}
/**
* 如果调用shiro的login认证失败,会回调这个方法
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
log.info("token认证失败");
return false;
}
}
JwtFilter拦截器的处理逻辑跟登录接口是差不多的,生成对应的token并调用subject.login(token)
@PostMapping("doLogin")
@ResponseBody
public Object doLogin(@RequestParam String username, @RequestParam String password, boolean rememberMe) {
String token;
try {
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
if (rememberMe) usernamePasswordToken.setRememberMe(true);
Subject subject = SecurityUtils.getSubject();
subject.login(usernamePasswordToken);
// 生成token
Map<String, Object> map = new HashMap<>();
map.put(JwtUtils.USERNAME, username);
token = JwtUtils.generateToken(map);
} catch (IncorrectCredentialsException ice) {
return HttpResult.error("password error!");
} catch (UnknownAccountException uae) {
return HttpResult.error("username error!");
}
return HttpResult.ok(token);
}
在ShrioConfig#shiroFilter,注入到Shiro并使用
@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
shiroFilterFactoryBean.setLoginUrl("/login");
// 设置无权限时跳转的 url
shiroFilterFactoryBean.setUnauthorizedUrl("/401");
// 注入自定义拦截器
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("autht", new JwtFilter());
shiroFilterFactoryBean.setFilters(filters);
// url拦截规则
Map<String, String> definitionMap = new LinkedHashMap<>();
definitionMap.put("/home", "anon");
definitionMap.put("/register", "anon");
definitionMap.put("/login", "anon");
definitionMap.put("/doLogin", "anon");
definitionMap.put("/401", "anon");
definitionMap.put("/logout", "anon");
// 禁用session创建,还需要noSessionCreation
definitionMap.put("/**", "noSessionCreation,autht");
shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);
return shiroFilterFactoryBean;
}
5 JwtUtils token工具类
package com.yzm.shiro09.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* JWT工具类
*/
public class JwtUtils implements Serializable {
private static final long serialVersionUID = 8527289053988618229L;
/**
* token头
* token前缀
*/
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Basic ";
/**
* 用户名称
*/
public static final String USERNAME = Claims.SUBJECT;
public static final String PASSWORD = "password";
/**
* 权限列表
*/
public static final String AUTHORITIES = "authorities";
/**
* 密钥
*/
private static final String SECRET = "abcdefg";
private static final String JWT_SECRET = "7786df7fc3a34e26a61c034d5ec8245d";
/**
* 过期时间5分钟
* 刷新时间2分钟
*/
public static final long TOKEN_EXPIRED_TIME = 5 * 60 * 1000L;
public static final long TOKEN_REFRESH_TIME = 2 * 60 * 1000L;
public static String generateToken(Map<String, Object> claims) {
return generateToken(claims, 0L);
}
/**
* 生成令牌
*/
public static String generateToken(Map<String, Object> claims, long expireTime) {
if (expireTime <= 0L) expireTime = TOKEN_EXPIRED_TIME;
Map<String, Object> headMap = new HashMap<>();
headMap.put("typ", "JWT");
headMap.put("alg", "HS256");
return Jwts.builder()
.setHeader(headMap)
//JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击
.setId(UUID.randomUUID().toString())
//.setIssuer("该JWT的签发者,是否使用是可选的")
//.setSubject("该JWT所面向的用户,是否使用是可选的")
//.setAudience("接收该JWT的一方,是否使用是可选的")
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
//签发时间(token生成时间)
.setIssuedAt(new Date())
//生效时间(在指定时间之前令牌是无效的)
.setNotBefore(new Date())
//过期时间(在指定时间之后令牌是无效的)
.setExpiration(new Date(System.currentTimeMillis() + expireTime))
//设置签名使用的签名算法和签名使用的秘钥
// .signWith(SignatureAlgorithm.HS256, JWT_SECRET)
.signWith(SignatureAlgorithm.HS256, generalKey())
.compact();
}
/**
* 验证令牌
*/
public static Claims verifyToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
//签名秘钥
// .setSigningKey(JWT_SECRET)
.setSigningKey(generalKey())
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
// token过期是直接抛出异常的,但仍然可以获取到claims对象
claims = e.getClaims();
}
return claims;
}
/**
* 由密钥生成加密key
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.decodeBase64(SECRET);
// return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return new SecretKeySpec(encodedKey, SignatureAlgorithm.HS256.getJcaName());
}
/**
* 是否过期
* true:过期
* false:未过期
*/
public static boolean isExpired(String token) {
Claims claims = verifyToken(token);
//和当前时间进行对比来判断是否过期
return claims.getExpiration().before(new Date());
}
/**
* 从令牌中获取用户名
*/
public static String getUsernameFromToken(String token) {
Claims claims = verifyToken(token);
return claims.getSubject();
}
/**
* 获取请求token
*/
public static String getTokenFromRequest(HttpServletRequest request) {
String token = request.getHeader(TOKEN_HEADER);
if (token == null) token = request.getHeader("token");
if (StringUtils.isBlank(token)) return null;
if (token.startsWith(TOKEN_PREFIX)) {
token = token.substring(TOKEN_PREFIX.length());
}
return token;
}
}
6 认证和授权
MyShiroRealm.java
package com.yzm.shiro09.config;
import com.yzm.shiro09.entity.Permissions;
import com.yzm.shiro09.entity.Role;
import com.yzm.shiro09.entity.User;
import com.yzm.shiro09.service.PermissionsService;
import com.yzm.shiro09.service.RoleService;
import com.yzm.shiro09.service.UserService;
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 java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 自定义realm
* 实现身份认证和权限授权
*/
public class MyShiroRealm extends AuthorizingRealm {
private final UserService userService;
private final RoleService roleService;
private final PermissionsService permissionsService;
public MyShiroRealm(UserService userService, RoleService roleService, PermissionsService permissionsService) {
this.userService = userService;
this.roleService = roleService;
this.permissionsService = permissionsService;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) principalCollection.getPrimaryPrincipal();
// 查询用户,获取角色ids
User user = userService.lambdaQuery().eq(User::getUsername, username).one();
List<Integer> roleIds = Arrays.stream(user.getRIds().split(","))
.map(Integer::parseInt)
.collect(Collectors.toList());
// 查询角色,获取角色名、权限ids
List<Role> roles = roleService.listByIds(roleIds);
Set<String> roleNames = new HashSet<>(roles.size());
Set<Integer> permIds = new HashSet<>();
roles.forEach(role -> {
roleNames.add(role.getRName());
Set<Integer> collect = Arrays.stream(
role.getPIds().split(",")).map(Integer::parseInt).collect(Collectors.toSet());
permIds.addAll(collect);
});
// 获取权限名称
List<Permissions> permissions = permissionsService.listByIds(permIds);
List<String> permNames = permissions.stream().map(Permissions::getPName).collect(Collectors.toList());
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(roleNames);
authorizationInfo.addStringPermissions(permNames);
return authorizationInfo;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户名跟密码
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
// 也可以这样获取
//String username = (String) authenticationToken.getPrincipal();
//String password = new String((char[]) authenticationToken.getCredentials());
// 查询用户是否存在
User user = userService.lambdaQuery().eq(User::getUsername, username).one();
if (user == null) {
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
// 用户名 + 盐
new MySimpleByteSource(user.getUsername() + user.getSalt()),
getName()
);
}
}
JwtRealm.java
package com.yzm.shiro09.config;
import com.yzm.shiro09.entity.JwtToken;
import com.yzm.shiro09.entity.User;
import com.yzm.shiro09.service.UserService;
import com.yzm.shiro09.utils.JwtUtils;
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;
public class JwtRealm extends AuthorizingRealm {
private final UserService userService;
public JwtRealm(UserService userService) {
this.userService = userService;
}
/**
* 限定这个Realm只支持我们自定义的JWT Token
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) authToken;
String token = jwtToken.getToken();
String username = JwtUtils.getUsernameFromToken(token);
if (username == null) {
throw new AuthenticationException("token过期,请重新登录");
}
User user = userService.lambdaQuery().eq(User::getUsername, username).one();
if (user == null) {
throw new AuthenticationException("账号异常");
}
return new SimpleAuthenticationInfo(
username,
token,
getName());
}
/**
* 由于在MyShiroRealm#doGetAuthorizationInfo中以及进行授权了,所以这里可以直接返回空的角色权限
*
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return new SimpleAuthorizationInfo();
}
}
JwtRealm的凭证信息是token,所以我们需要自定义凭证匹配器JwtCredentialsMatcher,主要就是校验下token是否过期
JwtCredentialsMatcher.java
package com.yzm.shiro09.config;
import com.yzm.shiro09.entity.JwtToken;
import com.yzm.shiro09.utils.JwtUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
/**
* 校验token是否过期
*/
public class JwtCredentialsMatcher implements CredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
if (authenticationToken instanceof JwtToken) {
String token = (String) authenticationInfo.getCredentials();
return !JwtUtils.isExpired(token);
}
return false;
}
}
7 ShiroConfig 配置类
package com.yzm.shiro09.config;
import com.yzm.shiro09.service.PermissionsService;
import com.yzm.shiro09.service.RoleService;
import com.yzm.shiro09.service.UserService;
import com.yzm.shiro09.utils.EncryptUtils;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import javax.servlet.Filter;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
@Configuration
public class ShiroConfig {
private final UserService userService;
private final RoleService roleService;
private final PermissionsService permissionsService;
public ShiroConfig(UserService userService, RoleService roleService, PermissionsService permissionsService) {
this.userService = userService;
this.roleService = roleService;
this.permissionsService = permissionsService;
}
/**
* 凭证匹配器
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName(EncryptUtils.ALGORITHM_NAME);
hashedCredentialsMatcher.setHashIterations(EncryptUtils.HASH_ITERATIONS);
//hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
/**
* 用户realm
*/
@Bean
public MyShiroRealm shiroRealm() {
MyShiroRealm shiroRealm = new MyShiroRealm(userService, roleService, permissionsService);
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
// 开启缓存
shiroRealm.setCachingEnabled(true);
//启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
shiroRealm.setAuthenticationCachingEnabled(true);
//缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
shiroRealm.setAuthenticationCacheName("authenticationCache");
//启用授权缓存,即缓存AuthorizationInfo信息,默认false
shiroRealm.setAuthorizationCachingEnabled(true);
//缓存AuthorizationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
shiroRealm.setAuthorizationCacheName("authorizationCache");
return shiroRealm;
}
/**
* jwtRealm
*/
@Bean
public JwtRealm jwtRealm() {
JwtRealm jwtRealm = new JwtRealm(userService);
jwtRealm.setCredentialsMatcher(new JwtCredentialsMatcher());
return jwtRealm;
}
/**
* 记住我功能
*/
@Bean
public Cookie simpleCookie() {
SimpleCookie cookie = new SimpleCookie("rememberMe");
//设为true后,只能通过http访问,javascript无法访问
//防止xss读取cookie
cookie.setHttpOnly(true);
cookie.setPath("/");
//存活时间,单位秒;-1表示关闭浏览器该cookie失效
cookie.setMaxAge(-1);
return cookie;
}
@Bean
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
rememberMeManager.setCookie(simpleCookie());
//cookie加密的密钥
//rememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return rememberMeManager;
}
/**
* 禁用session, 不保存用户登录状态。保证每次请求都重新认证
*/
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
/**
* redis缓存
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost("127.0.0.1:6379");
redisManager.setPassword("1234");
redisManager.setDatabase(0);
return redisManager;
}
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
// redis中针对不同用户缓存
redisCacheManager.setPrincipalIdFieldName("username");
// 用户权限信息缓存时间
redisCacheManager.setExpire(300);
return redisCacheManager;
}
/**
* 安全管理
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置单个realm
//securityManager.setRealm(shiroRealm());
// 验证器
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
securityManager.setAuthenticator(authenticator);
// 自定义realm 一定要放在securityManager.authorizer赋值之后(因为调用setRealms会将realms设置给authorizer
securityManager.setRealms(Arrays.asList(shiroRealm(), jwtRealm()));
// 记住我
securityManager.setRememberMeManager(rememberMeManager());
// 缓存
securityManager.setCacheManager(redisCacheManager());
// 关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 开启注解方式控制访问url
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
shiroFilterFactoryBean.setLoginUrl("/login");
// 设置无权限时跳转的 url
shiroFilterFactoryBean.setUnauthorizedUrl("/401");
// 注入自定义拦截器
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("autht", new JwtFilter());
shiroFilterFactoryBean.setFilters(filters);
// url拦截规则
Map<String, String> definitionMap = new LinkedHashMap<>();
definitionMap.put("/home", "anon");
definitionMap.put("/register", "anon");
definitionMap.put("/login", "anon");
definitionMap.put("/doLogin", "anon");
definitionMap.put("/401", "anon");
definitionMap.put("/logout", "anon");
// 禁用session创建,还需要noSessionCreation
definitionMap.put("/**", "noSessionCreation,autht");
shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);
return shiroFilterFactoryBean;
}
/**
* 解决: 登录页以及无权限页面不跳转问题
*/
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
Properties properties = new Properties();
// 登录后没有权限跳转到/401
properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "/401");
// 未登录访问接口跳转到/login
properties.setProperty("org.apache.shiro.authz.UnauthenticatedException", "/login");
simpleMappingExceptionResolver.setExceptionMappings(properties);
return simpleMappingExceptionResolver;
}
}
测试
登录yzm
访问/user/select
在这里我们设置了不创建Session
@Bean
public ShiroFilterFactoryBean shiroFilter() {
...
// 注入自定义拦截器
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("autht", new JwtFilter());
shiroFilterFactoryBean.setFilters(filters);
// url拦截规则
...
// 禁用session创建,还需要noSessionCreation
definitionMap.put("/**", "noSessionCreation,autht");
shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);
return shiroFilterFactoryBean;
}
并且在UserController.java中获取会话
@GetMapping
public Object user() {
// 当使用noSessionCreation时,这里就会报错
System.out.println("会话ID:" + SecurityUtils.getSubject().getSession().getId());
return SecurityUtils.getSubject().getPrincipal();
}
访问/user,会报错提示Session creation has been disabled for the current subject(已为当前主题禁用会话创建)