1、功能实现
1.整合JWT(JWT能很好的实现单点登录)
表单提交认证,认证成功后返回token,之后的请求携带token进行访问;
之前只需要认证一次,并将用户信息存储到session中;
使用jwt(无状态)之后,每次请求都需要重新认证,不用session,禁用session
2.同时整合swagger以便测试
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>
<!-- swagger -->
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>1.9.1.RELEASE</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://192.168.192.128:3306/testdb2?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
password: 1234
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、swagger配置
接口文档,方便测试
设置全局参数Authorization 值为token字符串
package com.yzm.shiro09.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.yzm.shiro09.controller"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(getGlobalParameters())
;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("swagger服务")
.description("swagger服务 API 接口文档")
.version("1.0.0")
.contact(new Contact("跳转网址", "https://www.baidu.com", "联系邮箱"))
.build();
}
/**
* 全局参数
*/
private List<Parameter> getGlobalParameters() {
// 添加请求参数,我们这里把token作为请求头部参数传入后端
List<Parameter> parameters = new ArrayList<>();
parameters.add(
new ParameterBuilder()
.name("Authorization")
.description("令牌")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false)
.build());
return parameters;
}
}
4、jwt工具类
主要提供生成token、判断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;
}
}
5、登录认证
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 lombok.extern.slf4j.Slf4j;
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 org.apache.shiro.util.ByteSource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 自定义Realm,实现认证和授权
* AuthorizingRealm 继承 AuthenticatingRealm
* AuthorizingRealm 提供 授权方法 doGetAuthorizationInfo
* AuthenticatingRealm 提供 认证方法 doGetAuthenticationInfo
*/
@Slf4j
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) {
log.info("授权");
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 {
log.info("认证");
// 获取用户名跟密码
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
// 查询用户是否存在
User user = userService.lambdaQuery().eq(User::getUsername, username).one();
if (user == null) {
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
// 用户名 + 盐
ByteSource.Util.bytes(user.getUsername() + user.getSalt()),
getName()
);
}
}
6、Jwt认证
supports():决定走哪一种认证方式
package com.yzm.shiro09.config;
import com.yzm.shiro09.entity.User;
import com.yzm.shiro09.service.UserService;
import com.yzm.shiro09.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
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 {
log.info("jwt认证");
JwtToken jwtToken = (JwtToken) authToken;
String token = jwtToken.getToken();
String username = JwtUtils.getUsernameFromToken(token);
User user = userService.lambdaQuery().eq(User::getUsername, username).one();
if (user == null) {
throw new UnknownAccountException("账号异常");
}
return new SimpleAuthenticationInfo(
username,
token,
getName());
}
/**
* 由于在MyShiroRealm#doGetAuthorizationInfo中已经进行授权了,所以这里可以直接返回空的角色权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("jwt授权");
return new SimpleAuthorizationInfo();
}
}
表单认证使用的是UsernamePasswordToken
以便实现不同的认证逻辑,需要自定义JwtToken
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;
}
}
7、Jwt过滤器
拦截所有请求,除了 anon 修饰的请求
isAccessAllowed() 拦截器的入口
createToken() 获取请求头的token,生成JwtToken
package com.yzm.shiro09.config;
import com.yzm.common.utils.HttpUtils;
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 = super.executeLogin(request, response);
} catch (IllegalStateException e) {
log.error("Not found any token:" + WebUtils.toHttp(request).getRequestURI());
} catch (Exception e) {
log.error("Error occurs when login", e);
}
return allowed || super.isPermissive(mappedValue);
}
/**
* 这里重写了父类的方法,使用我们自己定义的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;
}
/**
* 拒绝处理
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpUtils.errorWrite((HttpServletResponse) response, "请求失败");
return false;
}
/**
* Login认证成功,会进入该方法
* 我们这里判断了是否要刷新Token,始终返回可用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();
Claims claims = JwtUtils.verifyToken(tokenStr);
long expireTime = claims.getExpiration().getTime();
// 当前时间 + 刷新时间 >= 过期时间,则进行刷新token
if (System.currentTimeMillis() + JwtUtils.TOKEN_REFRESH_TIME >= expireTime) {
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;
}
/**
* Login认证失败,会回调这个方法
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
log.info("token认证失败");
if (e instanceof IncorrectCredentialsException) {
log.error("token过期,请重新登录");
}
if (e instanceof UnknownAccountException) {
log.error("账号异常");
}
return false;
}
}
jwt凭证匹配器
jwt认证不需要校验密码,验证token是否过期需要自己实现
package com.yzm.shiro09.config;
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;
}
}
8、ShrioConfig 配置类
验证器ModularRealmAuthenticator的作用:
当我们提供多个认证Realm时,告诉Shiro我们想要的认证策略
1.AtLeastOneSuccessfulStrategy:至少一个认证通过,返回所有认证通过的认证对象信息
2.FirstSuccessfulStrategy:跟AtLeastOneSuccessfulStrategy一样,不同的是只返回第一个认证通过的认证对象信息
3.AllSuccessfulStrategy:所有认证都要通过才算认证通过,并返回所有认证对象信息
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.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
@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);
return hashedCredentialsMatcher;
}
/**
* 自定义Realm
*/
@Bean
public MyShiroRealm shiroRealm() {
MyShiroRealm shiroRealm = new MyShiroRealm(userService, roleService, permissionsService);
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
/**
* jwtRealm
*/
@Bean
public JwtRealm jwtRealm() {
JwtRealm jwtRealm = new JwtRealm(userService);
jwtRealm.setCredentialsMatcher(new JwtCredentialsMatcher());
return jwtRealm;
}
/**
* 禁用session, 不保存用户登录状态。保证每次请求都重新认证
*/
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
/**
* 安全管理
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 验证器、认证策略
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
securityManager.setAuthenticator(authenticator);
// 自定义realm 一定要放在securityManager.authorizer赋值之后(因为调用setRealms会将realms设置给authorizer
securityManager.setRealms(Arrays.asList(shiroRealm(), jwtRealm()));
// 关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 开启注解
*/
@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());
// 注入自定义拦截器
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("autht", new JwtFilter());
shiroFilterFactoryBean.setFilters(filters);
Map<String, String> definitionMap = new LinkedHashMap<>();
// swagger放行
definitionMap.put("/swagger**", "anon");
definitionMap.put("/webjars/**", "anon");
definitionMap.put("/swagger-resources/**", "anon");
definitionMap.put("/v2/**", "anon");
definitionMap.put("/login", "anon");
// 禁用session创建,还需要noSessionCreation
definitionMap.put("/**", "noSessionCreation,autht");
shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);
return shiroFilterFactoryBean;
}
}
9、测试
访问 http://localhost:8080/swagger-ui.html
/login请求登录,不被 JWT过滤器拦截,正常获取到token
携带token 访问/user/select,会被jwt过滤器拦截进行重新认证授权
访问/user/delete,提示权限不足,每次请求,返回的响应头都携带着可用token
我们设置了禁用Session
/**
* 禁用session, 不保存用户登录状态。保证每次请求都重新认证
*/
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
/**
* 安全管理
*/
@Bean
public SecurityManager securityManager() {
...
// 关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter() {
...
// 禁用session创建,还需要noSessionCreation
definitionMap.put("/**", "noSessionCreation,autht");
shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);
return shiroFilterFactoryBean;
}
在UserController.java中获取会话Id
@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(已为当前主题禁用会话创建)
当token过期时,访问接口认证失败,走到下面的流程时
AuthenticationException不能转化成IncorrectCredentialsException和UnknownAccountException,
只会打印token认证失败
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
log.info("token认证失败");
if (e instanceof IncorrectCredentialsException) {
log.error("token过期,请重新登录");
}
if (e instanceof UnknownAccountException) {
log.error("账号异常");
}
return false;
}
解决方法:自定义认证器,重写doMultiRealmAuthentication()方法
package com.yzm.shiro09.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.AuthenticationStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import java.util.Collection;
/**
* 自定义认证器,解决 Shiro 异常无法返回的问题
* 父类使用Throwable捕获异常,并不往上抛出
* 这里改为AuthenticationException来接收异常,并往上抛出
*/
@Slf4j
public class MultiRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token)
throws AuthenticationException {
AuthenticationStrategy strategy = this.getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
AuthenticationException authenticationException = null;
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (AuthenticationException e) {
// 默认是使用Throwable来接收异常
authenticationException = e;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm
+ "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, e);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, authenticationException);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
if (authenticationException != null) {
throw authenticationException;
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
}
在ShiroConfig#securityManager
/**
* 安全管理
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置单个realm
//securityManager.setRealm(shiroRealm());
// 自定义验证器
ModularRealmAuthenticator authenticator = new MultiRealmAuthenticator();
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
securityManager.setAuthenticator(authenticator);
...
return securityManager;
}