java入门笔记五(springboot框架博客系统开发2)
继续博客网站后端的开发。
引入shiro和jwt. shiro用来缓存和会话信息,存储在redis里面,jwt作为跨域身份验证解决方案。
首先是常规jwt的逻辑图:
引入shiro之后的逻辑图
1 导入shiro和jwt,主要用于缓存和会话信息。首先需要装一下redis,本机mac安装步骤如下,安装之后配置自启动
step1:去官网下载redis稳定版
step2:解压,移动到你的mac的/user/local目录下
step3:开始执行命令:make 然后 make install
step4:进入redis的安装目录,启动redis服务:redis-server
step5 如果你想要你的redis在后台启动,修改redis.conf中的daemonize yes,然后重启服务:redis-server ./redis.conf
2 pom.xml导入shiro-redis的starter包:还有jwt的工具包,以及hutool工具包(utils类包)。
<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>
3 重写shiro配置文件,首先新建一个com.blog.config.ShiroConfig类,意义如下
- 引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。
- 重写了SessionManager和DefaultWebSecurityManager,同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,我们需要设置为false,这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。
- 在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径,而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication,这样控制权限访问。
代码如下:
package com.blog.config; import com.blog.shiro.AccountRealm; import com.blog.shiro.JwtFilter; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition; import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; /** * shiro启用注解拦截控制器 */ @Configuration public class ShiroConfig { @Autowired JwtFilter jwtFilter; /** * session域管理 * @param * @return */ @Bean public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); // inject redisSessionDAO 注入 sessionManager.setSessionDAO(redisSessionDAO); // other stuff... return sessionManager; } /** * 重写shiro的安全管理容器, * @param * @param sessionManager * @param redisCacheManager * @return */ @Bean public DefaultWebSecurityManager securityManager(AccountRealm accountRealm, SessionManager sessionManager, RedisCacheManager redisCacheManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm); //inject sessionManager securityManager.setSessionManager(sessionManager); // inject redisCacheManager securityManager.setCacheManager(redisCacheManager); // other stuff... //关闭shiro自带的session,详情见文档 // DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); // DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); // defaultSessionStorageEvaluator.setSessionStorageEnabled(false); // subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); // securityManager.setSubjectDAO(subjectDAO); return securityManager; } /** * 在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径, * 而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息, * 有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截, * 比如@RequiresAuthentication,这样控制权限访问。 * @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 * @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; } }
4 写shiro包的类,首先新建com.blog.shiro.AccountRealm类,继承shiro的AuthorizingRealm类并重写三个方法,
- supports:为了让realm支持jwt的凭证校验
- doGetAuthorizationInfo:权限校验
- doGetAuthenticationInfo:登录认证校验
代码如下
package com.blog.shiro; import cn.hutool.core.bean.BeanUtil; import com.blog.entity.User; import com.blog.service.UserService; import com.blog.utils.JwtUtils; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Slf4j //可以使用log打印日志 @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 principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } /** * 登录认证 * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { JwtToken jwt = (JwtToken) token; // 将传入的AuthenticationToken强转JwtTokenJ log.info("jwt----------------->{}", jwt); // 获取jwtToken中的userId String userId = jwtUtils.getClaimByToken((String)jwt.getPrincipal()).getSubject(); // 根据jwtToken中的userId查询数据库 User user = userService.getById(Long.parseLong(userId)); if(user == null) { throw new UnknownAccountException("账户不存在!"); } if(user.getStatus() == -1) { throw new LockedAccountException("账户已被锁定!"); } // 将可以显示的信息放在该载体中,对于密码这种隐秘信息不需要放在该载体中 AccountProfile profile = new AccountProfile(); BeanUtil.copyProperties(user, profile); log.info("profile----------------->{}", profile.toString()); //封装成SimpleAuthenticationInfo返回给shiro return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName()); } }
5 创建JwtToken类,新建com.blog.shiro.JwtToken类,用来实现AuthenticationToken接口,因为shiro默认supports
支持的是UsernamePasswordToken,而我们采用jwt的方式,故需要定义一个JwtToken来重写该token。
package com.blog.shiro; import org.apache.shiro.authc.AuthenticationToken; /** * 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; } }
6 接着创建工具类com.blog.utils.JwtUtils类,用于生成和校验jwt,其中jwt相关的密钥信息是从项目的配置文件中获取的。
package com.blog.utils; import io.jsonwebtoken.Claims; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Date; @Slf4j //可以使用log打印日志 @Data @Component @ConfigurationProperties(prefix="blog.jwt") //配合application.properties 中加入的配置 public class JwtUtils { private String secret; private long expire; private String header; /** * 生成jwt token */ public String generateToken(long userId) { return null; } // 获取jwt的信息 public Claims getClaimByToken(String token) { return null; } /** * token是否过期 * @return true:过期 */ public boolean isTokenExpired(Date expiration) { return expiration.before(new Date()); } }
在之前新建的src/main/resources/application.yml追加配置如下:
shiro-redis: enabled: true redis-manager: host: 127.0.0.1:6379 blog: jwt: # 加密秘钥 secret: f4e2e52034348f86b67cde581c0f9abc # token有效时长,7天,单位秒 expire: 604800 header: token
7 加上登录成功返回的一个用户信息类AccountProfile,新增com.blog.shiro.AccountProfile类,
package com.blog.shiro; import lombok.Data; import java.io.Serializable; @Data public class AccountProfile implements Serializable { private Long id; private String username; private String avatar; }
8 定义jwt的过滤器JwtFilter,继承Shiro内置的AuthenticatingFilter,内置了可以自动登录方法的的过滤器。重写4个方法
createToken:实现登录,我们需要生成我们自定义支持的JwtToken
onAccessDenied:拦截校验,当头部没有Authorization时候,我们直接通过,不需要自动登录;
当带有的时候,首先我们校验jwt的有效性,没问题我们就直接执行executeLogin方法实现自动登录
onLoginFailure:登录异常时候进入的方法,我们直接把异常信息封装然后抛出
preHandle:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,
我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。
package com.blog.shiro; import cn.hutool.json.JSONUtil; import com.blog.common.lang.Result; import com.blog.utils.JwtUtils; import io.jsonwebtoken.Claims; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.ExpiredCredentialsException; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import org.apache.shiro.web.filter.authc.AuthenticationFilter; import org.apache.shiro.web.util.WebUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; 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.io.IOException; @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 { // 获取 token HttpServletRequest request = (HttpServletRequest) servletRequest; String jwt = request.getHeader("Authorization"); if(StringUtils.isEmpty(jwt)){ return null; } return new JwtToken(jwt); } /** * 拦截校验 * @param servletRequest * @param servletResponse * @return * @throws Exception */ @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest) servletRequest; String token = request.getHeader("Authorization"); //没有token,直接通过 if(StringUtils.isEmpty(token)) { return true; } else { // 判断是否已过期 Claims claim = jwtUtils.getClaimByToken(token); 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 httpResponse = (HttpServletResponse) response; try { //处理登录失败的异常 Throwable throwable = e.getCause() == null ? e : e.getCause(); //获取登陆异常信息以自定义的Resut响应格式返回json数据 Result r = Result.fail(throwable.getMessage()); String json = JSONUtil.toJsonStr(r); httpResponse.getWriter().print(json); } catch (IOException e1) { } return false; } /** * 对跨域提供支持 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { 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,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
shiro整合完毕,并且使用了jwt进行身份校验。