一、整合流程逻辑
二、整合步骤
1. 导入shiro-redis的starter包:还有jwt的工具包,以及为了简化开发,我引入了hutool工具包。
<!--shiro-redis整合-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.2.1</version>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2. 编写配置
引入RedisSessionDAO和RedisCacheManager,实现将shiro权限数据和会话信息保存到redis中,实现会话共享。
重写 shiro中的SessionManager和DefaultWebSecurityManager,同时在重写的DefaultWebSecurityManager中关闭shiro自 带的session,需要设置位false,这样用户将不能通过session方式登陆shiro。后面采用jwt凭证登陆。
重写 shiro的ShiroFilterChainDefinition 注册自己的过滤器。我们将不再通过编码方式拦截访问路径,而是所有路径通过自己注册的JwtFilter过滤器,然后判断是否有jwt凭证,有则登陆,无则跳过,跳过之后,有shiro的权限注解进行拦截,eg:@RequiredAuthentication,这样控制权限访问。
@Configuration public class ShiroConfig { @Autowired JwtFilter jwtFilter; /** * session域管理 * @param redisSessionDAO * @return */ @Bean public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); // inject redisSessionDAO sessionManager.setSessionDAO(redisSessionDAO); return sessionManager; }
/**
* 重写shiro的安全管理容器,
* @param accountRealm
* @param sessionManager
* @param redisCacheManager
* @return
*/
@Bean
public SessionsSecurityManager securityManager(AccountRealm accountRealm, SessionManager sessionManager, RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
//inject sessionManager
securityManager.setSessionManager(sessionManager);
// inject redisCacheManager
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
/**
* 定义过滤器
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
// 申请一个默认的过滤器链
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String,String> filterMap = new LinkedHashMap<>();
//添加一个jwt过滤器到过滤器链中
filterMap.put("/**","jwt");
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
/**
* 过滤器工厂业务
* @param securityManager shiro中的安全管理
* @param shiroFilterChainDefinition
* @return
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition){
/*shiro过滤器bean对象*/
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// 需要添加的过滤规则
Map<String,Filter> filters = new HashMap<>();
filters.put("jwt",jwtFilter);
shiroFilter.setFilters(filters);
Map<String,String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
}
### 3. 编写realm
AccountRealm shiiro进行登陆或者权限校验的逻辑。
需要重写三个方法。
- supports:为了使realm支持jwt的凭证校验
- doGetAuthorizationInfo:权限校验
- doGetAuthenticationInfo:登陆认证校验
```java
@Slf4j
@Component
public class AccountRealm extends AuthorizingRealm{
@Autowired
JwtUtils jwtUtils;
@Autowired
UserService userService;
/**
* 判断是否为jwt的token
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 权限验证
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 登陆认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 将传入的AuthenticationToken强转JwtToken
JwtToken jwtToken = (JwtToken) authenticationToken;
// 获取jwtToken中的userId
String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();
// 根据jwtToken中的userId查询数据库
User user = userService.getById(Long.valueOf(userId));
if(user == null){
throw new UnknownAccountException("账户不存在!");
}
if(user.getStatus() == -1){
throw new LockedAccountException("账户已被锁定!");
}
// 将可以显示的信息放在该载体中,对于密码这种隐秘信息不需要放在该载体中
AccountProfile accountProfile = new AccountProfile();
BeanUtils.copyProperties(user,accountProfile);
log.info("jwt------------->{}",jwtToken);
// 将token中用户的基本信息返回给shiro
return new SimpleAuthenticationInfo(accountProfile,jwtToken.getCredentials(),getName());
}
}
主要配置doGetAuthenticationInfo登陆认证这个方法,通过jwt凭证获取用户信息,判断用户的状态,最后异常就抛出相应的异常信息。
4.编写JwtToken
shiro默认supports支持的是UsernamePasswordToken,而我们采用jwt的方式,故需要定义一个JwtToken来重写该token。
public class JwtToken implements AuthenticationToken{
private String token;
public JwtToken(String token){
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
5.编写JwtUtils生成和校验jwt的工具类
有些jwt相关的密钥信息是从项目的配置文件中获取的。
@Component
@ConfigurationProperties(prefix = "mt.vuemtblog.jwt")
public class JwtUtils {
private String secret;
private long expire;
private String header;
/**
* 生成jwt token
* @param userId
* @return
*/
public static String generateToken(long userId){
return null;
}
/**
* 获取jwt的信息
* @param token
* @return
*/
public static Claims getClaimByToken(String token){
return null;
}
/**
* 验证token是否过期
* @param expiration
* @return true 过期
*/
public static boolean isTokenExpired(Date expiration){
return expiration.before(new Date());
}
}
6.编写登陆成功返回用户信息的载体AccountProfile
@Data
public class AccountProfile implements Serializable {
private Long id;
private String username;
private String avatar;
}
7. 全局配置基本信息
shiro-redis:
enabled: true
redis-manger:
host:127.0.0.1:6379
mt:
vuemtblog:
jwt:
#加密密钥
secret:f4e2e52034348f86b67cde581c0f9eb5
# token 有效时长 7天 单位秒
expire:604800
# 设定token在header中的键值
header:authorization
8. 若项目使用spring-boot-devtools,需要添加一个配置文件,
在resources目录下新建META-INF,然后新建spring-devtools.properties,这样热重启就不会报错。
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
9. 编写自定义的JwtFileter过滤器
这里我们继承的是Shiro内置的AuthenticatingFilter,一个可以内置了可以自动登录方法的的过滤器,有些同学继承BasicHttpAuthenticationFilter也是可以的。
我们需要重写几个方法:
createToken:实现登录,我们需要生成我们自定义支持的JwtToken
onAccessDenied:拦截校验,当头部没有Authorization时候,我们直接通过,不需要自动登录;当带有的时候,首先我们校验jwt的有效性,没问题我们就直接执行executeLogin方法实现自动登录
onLoginFailure:登录异常时候进入的方法,我们直接把异常信息封装然后抛出
preHandle:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。
@Component public class JwtFilter extends AuthenticatingFilter{ @Autowired JwtUtils jwtUtils; /** * 实现登陆,生成自定义的JwtToken * @param servletRequest * @param servletResponse * @return * @throws Exception */ @Override protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest)servletRequest; String jwt = request.getHeader("Authorization"); if(StringUtils.isEmpty(jwt)){ return null; } return new JwtToken(jwt); } /** * 拦截校验 * @description 当头部没有Authorization,直接通过,不需要自动登陆。 * 当带有Authorization时,需要先校验jwt的时效性,没问题直接执行executeLogin实现自动登陆,将token委托给shiro。 * @param servletRequest * @param servletResponse * @return * @throws Exception */ @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest) servletRequest; // 获取用户请求头中的token String token = request.getHeader("Authorization"); if (StringUtils.isEmpty(token)) {// 没有token return true; } else { // 校验jwt Claims claim = jwtUtils.getClaimByToken(token); // tonken为空或者时间过期 if (claim == null || jwtUtils.isTokenExpired((claim.getExpiration()))) { throw new ExpiredCredentialsException("token以失效,请重新登陆!"); } } // 执行自动登陆 return executeLogin(servletRequest, servletResponse); } /** * 执行登录出现异常 * @param token * @param e * @param request * @param response * @return */ @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpServletResponse = (HttpServletResponse)response; // 1. 判断是否因异常登陆失败 Throwable throwable = e.getCause() == null ? e : e.getCause(); // 2.获取登陆异常信息以自定义的Resut响应格式返回json数据 Result result = Result.error(throwable.getMessage()); String json = JSONUtil.toJsonStr(result);// hutool的一个json工具 // 3.打印响应 try{ httpServletResponse.getWriter().print(json); }catch (IOException ioException){ } return false; } }
三、springboot中全局异常处理
前后端分离,我们需要配置异常处理机制,返回一个友好简单格式给前端。
处理方式:
通过@ControllerAdvice来进行统一异常处理
通过@ExceptionHandler(value=RuntimeException.class)指定要捕获的Exception的各个类型,这个异常处理是全局的,所有的类似异常都会捕获。
/** * 全局异常处理 */ @Slf4j @RestControllerAdvice public class GlobalExcepitonHandler { // 捕捉shiro的异常 @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(ShiroException.class) public Result handle401(ShiroException e) { return Result.fail(401, e.getMessage(), null); } /** * 处理Assert的异常 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = IllegalArgumentException.class) public Result handler(IllegalArgumentException e) throws IOException { log.error("Assert异常:-------------->{}",e.getMessage()); return Result.fail(e.getMessage()); } /** * @Validated 校验错误异常处理 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MethodArgumentNotValidException.class) public Result handler(MethodArgumentNotValidException e) throws IOException { log.error("运行时异常:-------------->",e); // 截取所有必要的错误信息;只显示错误原因,不会显示其他cause BY.... BindingResult bindingResult = e.getBindingResult(); ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get(); return Result.fail(objectError.getDefaultMessage()); } /* * 运行时异常 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = RuntimeException.class) public Result handler(RuntimeException e) throws IOException { log.error("运行时异常:-------------->",e); return Result.fail(e.getMessage()); } }
上面我们捕捉了几个异常:
ShiroException:shiro抛出的异常,比如没有权限,用户登录异常
IllegalArgumentException:处理Assert的异常
MethodArgumentNotValidException:处理实体校验的异常
RuntimeException:捕捉其他异常
1. springboot中实体校验
使用springboot框架,就自动集成了Hibernate validatior。
第一步:实体属性上添加校验规则
@TableName("m_user") public class User implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; @NotBlank(message = "昵称不能为空") private String username; private String avatar; @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; }
第二步:测试实体校验
采用@Validated注解,实体中有不符合校验规则的,会抛出异常,在异常处理中的MethodArgumentNotValidException中捕获。
@PostMapping("/save") public Object save(@Validated @RequestBody User user) { return user.toString(); }
四、前后端分离的跨域处理
在后台进行全局跨域处理 ```java /**
解决跨域问题
project: vue-mt-blog
created by Maotao on 2020/6/30
/ @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**"). allowedOrigins("*"). allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"). allowCredentials(true). maxAge(3600). allowedHeaders("*");
} }
全局跨域处理
```java
/**
* 解决跨域问题
* project: vue-mt-blog
* created by Maotao on 2020/6/30
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").
allowedOrigins("*").
allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS").
allowCredentials(true).
maxAge(3600).
allowedHeaders("*");
}
}
本文由博客一文多发平台 OpenWrite 发布!