1. Integrate process logic
Second, the integration steps
1. Import the starter package of shiro-redis: there is also the jwt toolkit, and in order to simplify development, I introduced the hutool toolkit.
<!--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. Write the configuration
- Introduce RedisSessionDAO and RedisCacheManager to save shiro permission data and session information in redis and share session.
- To rewrite SessionManager and DefaultWebSecurityManager in shiro, and close the session that comes with shiro in the rewritten DefaultWebSecurityManager, you need to set the bit to false, so that users will not be able to log in to shiro through session. Then use the jwt credentials to log in.
- Override Shiro's ShiroFilterChainDefinition to register your own filter. We will no longer intercept the access path by coding, but all paths will pass through the JwtFilter filter registered by ourselves, and then judge whether there is a jwt credential, if there is, log in, if not, skip, after skipping, there is shiro's permission annotation to intercept , eg: @RequiredAuthentication, which controls permission access.
@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. Write realm
The logic of AccountRealm shiiro for login or permission verification.
Three methods need to be overridden.
- supports: In order for realm to support jwt credential verification
- doGetAuthorizationInfo:Authorization verification
doGetAuthenticationInfo: Login authentication verification
@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()); } }
Mainly configure the doGetAuthenticationInfo login authentication method, obtain user information through jwt credentials, judge the status of the user, and finally throw the corresponding exception information when an exception occurs.
4. Write JwtToken
Shiro supports UsernamePasswordToken by default, and we use the jwt method, so we need to define a JwtToken to rewrite the 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. Write a tool class for JwtUtils to generate and verify jwt
Some jwt-related key information is obtained from the project's configuration file.
@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. Write a carrier AccountProfile that returns user information after successful login
@Data public class AccountProfile implements Serializable { private Long id; private String username; private String avatar; }
7. Global configuration basic information
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. If the project uses spring-boot-devtools, you need to add a configuration file,
Create a new META-INF in the resources directory, and then create a new spring-devtools.properties, so that the hot restart will not report an error.
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar
9. Write a custom JwtFileter filter
Here we inherit Shiro's built-in AuthenticatingFilter, a filter with built-in automatic login methods, and some students can inherit BasicHttpAuthenticationFilter.
We need to override several methods:
- createToken: To implement login, we need to generate our custom supported JwtToken
- onAccessDenied: Intercept verification, when there is no Authorization in the header, we pass directly without automatic login; when it is, first we verify the validity of jwt, no problem, we directly execute the executeLogin method to achieve automatic login
- onLoginFailure: The method to enter when the login is abnormal, we directly encapsulate the exception information and throw it
preHandle: The pre-interception of the interceptor, because we are a front-end and back-end analysis project, in addition to the need for cross-domain global configuration in the project, we also need to provide cross-domain support in the interceptor. In this way, the interceptor will not be restricted before entering the 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; } }
3. Global exception handling in springboot
To separate the front and back ends, we need to configure the exception handling mechanism and return a friendly and simple format to the front end.
Processing method:
- Unified exception handling through @ControllerAdvice
Specify each type of Exception to be caught by @ExceptionHandler(value=RuntimeException.class). This exception handling is global, and all similar exceptions will be caught.
/** * 全局异常处理 */ @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()); } }
Above we caught several exceptions:
- ShiroException: exception thrown by shiro, such as no permission, user login exception
- IllegalArgumentException: Handling Assert's exception
- MethodArgumentNotValidException: Handling entity validation exceptions
- RuntimeException: catch other exceptions
1. Entity verification in springboot
Using the springboot framework, Hibernate validatior is automatically integrated.
Step 1: Add validation rules to entity attributes
@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; }
Step 2: Test entity verification
With the @Validated annotation, if there are entities that do not meet the validation rules, an exception will be thrown, which will be caught in MethodArgumentNotValidException in exception handling.
@PostMapping("/save") public Object save(@Validated @RequestBody User user) { return user.toString(); }
4. Cross-domain processing with front-end and back-end separation
Global cross-domain processing in the background
/** * 解决跨域问题 * 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("*"); } }
Global cross-domain processing
/** * 解决跨域问题 * 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("*"); } }
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。