一、前言
因为最近家里面有一点事情,所以请假了四天,现在准备继续完成**shiro、jwt搭建系统(二)**的内容,下面我就根据(一)中的内容,结合我所学习到的知识,记录搭建系统的基本内容。
我们首先说一说我们系统的基本流程
- JwtFilter 主要用于拦截请求、校验请求体中是否存在token、校验token是否有效(是否过期、是否合法等等)、生成新的令牌等等操作
- 根据JwtFilter拦截器的结果(是否过期),如果过期,我们新生成新的令牌,并且将它存储到Redis和ThreadLocalToken中
- 每次请求(Controller)TokenAspect都需要判断ThreadLocalToken中是否有存储的Token,如果存在令牌,说明是重新新生成的令牌,将它返回给用户存储,并且清空ThreadLocalToken存储的令牌
写到这里,我们需要具体的讲一下登录流程,因为我们使用的是jwt来进行验证,后端关闭session,所以每一次请求都需要携带令牌
- 传统的验证,我们向后端提交username password,交给Shiro进行验证(subject.login(usernamePasswordToken)),如果验证成功将令牌进行持久化并且返回给用户,下一次登录就不需要再经过自定义的Realm
- 如果使用jwt shiro的方式,我们无需将登录的请求交给UserRealm处理,我们可以直接通过userId查询用户信息,获取到信息后直接返回令牌,但是每一次访问受限的资源,都需要通过JwtFilter然后将token提交到UserRealm进行校验
二、用ThreadLocal来存储Token
根据上面的流程图,其实我们也可以在JwtFilter拦截时,将新的token返回给用户,但是这样就会涉及到IO流解析、JSON字符串解析的问题,所以我们使用ThreadLocalToken来存储Token,每一次访问Controller都可以进行AOP拦截,将新生成的令牌,放入返回体中,这样就比较优雅
创建ThreadLocalToken
/**
* 自定义ThreadLocalToken
*/
@Component
public class ThreadLocalToken {
private ThreadLocal<String> threadLocal = new ThreadLocal<>();
/**
* 设置token
* @param token
*/
public void setToken(String token){
threadLocal.set(token);
}
/**
* 获取token
* @return
*/
public String getToken(){
return (String) threadLocal.get();
}
/**
* 移除内容
*/
public void remove(){
threadLocal.remove();
}
}
复制代码
三、创建JwtFilter拦截请求
主要用于拦截请求:
- 如果token过期,判断redis中的token是否有效,如果有效,生成新的令牌,如果也是同样的无效,就直接提示用户重新登录
- 判断请求是否需要交给shiro进行处理,如果是OPTIONS请求方法,就直接放行,不需要shiro进行处理
/**
* 自定义过滤器
* TODO: 用于拦截请求、校验令牌合法性、生成新的令牌
*/
@Component
@Scope("prototype")
public class JwtFilter extends AuthenticatingFilter {
@Autowired
private ThreadLocalToken threadLocalToken;
@Value("${gofly.jwt.cache-expire}")
private Integer cacheExpire;
@Autowired
private RedisTemplate redisTemplate;
/**
* 创建令牌
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
String token = getRequestToken((HttpServletRequest) servletRequest);
if(StrUtil.hasBlank(token)){
return null;
}
return new ShiroToken(token);
}
/**
* 判断请求是否需要shiro进行处理
* @param servletRequest
* @param servletResponse
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
//判断请求方法是否为OPTIONS
//因为ajax发起的请求,首先会发起OPTIONS请求,如果为OPTIONS请求,直接放行,如果不是,就交给Shiro进行处理
if(request.getMethod().equals(RequestMethod.OPTIONS.name())){
return true;
}
return false;
}
/**
* 用于所有需要shiro处理的请求
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//将响应头设置为application/json格式,返回值为json字符串
response.setHeader("Content-Type", "application/json;charset=UTF-8");
//允许跨域请求
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
//清空ThreadLocal存储的令牌
threadLocalToken.remove();
//校验令牌是否存在请求头或者请求参数中
String token = getRequestToken(request);
if(StrUtil.hasBlank(token)){
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
String message = JSONUtil.toJsonStr(R.error("令牌无效").put("code", HttpStatus.HTTP_UNAUTHORIZED));
response.getWriter().write(message);
return false;
}
//验证令牌合法性
try{
JwtUtil.verifierToken(token);
}catch (TokenExpiredException e){ //令牌过期
//1、判断Redis中的token是否过期,因为我们在配置的时候Redis存储的token过期时间是Jwt token的两倍,所以令牌过期,不一定redis存储的token也过期
if(redisTemplate.hasKey(token)){
//2、如果redis中存储的令牌未过期,我们就删除Redis存储的令牌,重新生成新的令牌
redisTemplate.delete(token);
int userId = JwtUtil.getUserId(token);//获取用户id
String newToken = JwtUtil.createToken(userId); //生成新的令牌
redisTemplate.opsForValue().set(newToken,userId+"",cacheExpire, TimeUnit.DAYS); //将新的令牌存储到Redis中
threadLocalToken.setToken(newToken);//将新的令牌存储到ThreadLocal中
}else{
//3、还有一种情况,jwt token过期,redis中的token也过期,需要提示用户重新登录
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
String message = JSONUtil.toJsonStr(R.error("令牌过期请重新登录").put("code", HttpStatus.HTTP_UNAUTHORIZED));
response.getWriter().write(message);
return false;
}
}catch (JWTDecodeException e){ //Jwt token解析失败
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
String message = JSONUtil.toJsonStr(R.error("令牌无效").put("code", HttpStatus.HTTP_UNAUTHORIZED));
response.getWriter().write(message);
return false;
}
/**
* 执行登录流程
* 也就是提交到{@link UserRealm}中进行校验
*/
boolean b = executeLogin(request, response);
return b;
}
/**
* 登录失败,调用
* @param token
* @param e
* @param servletRequest
* @param servletResponse
* @return
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest servletRequest, ServletResponse servletResponse) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
response.setContentType("application/json;charset=utf-8");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
try {
/**
* 这里接收的是{@link UserRealm}返回的错误信息
*/
response.getWriter().print(e.getMessage());
} catch (IOException exception) {
}
return false;
}
/**
* 获取请求体中的令牌
* @param request
* @return
*/
private String getRequestToken(HttpServletRequest request){
String token = null;
//请求头中获取令牌
token = request.getHeader("token");
//如果请求头中没有令牌,就从请求参数中获取
if(StrUtil.hasBlank(token)){
token = request.getParameter("token");
}
return token;
}
/**
* 将自定义的过滤器,添加到过滤器链中
* @param request
* @param response
* @param chain
* @throws ServletException
* @throws IOException
*/
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
super.doFilterInternal(request, response, chain);
}
}
复制代码
四、ShiroConfig-Shiro配置类
/**
* Shiro配置类
*/
@Configuration
public class ShiroConfig {
/**
* 安全管理器
* @return
*/
@Bean("securityManager")
public SecurityManager securityManager(UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
securityManager.setRememberMeManager(null);
return securityManager;
}
/**
* shiro过滤器链
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,JwtFilter jwtFilter){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//自定义过滤器
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt",jwtFilter);
shiroFilterFactoryBean.setFilters(filters);
Map<String,String> filterMap = new LinkedHashMap<>();
filterMap.put("/**/doc.html","anon");
filterMap.put("/pub/**","anon");
//除以上连接,都需要被jwtFilter拦截
filterMap.put("/**","jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* shiro生命周期
* @return
*/
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
复制代码
四、创建TokenAspect-用于返回最新的令牌
我们采用的是aop技术,将controller作为切点,主要完成以下工作:
- 通过环绕通知获取返回值
- 获取ThreadLocalToken中存储的token,如果存在,说明令牌被更新,如果不存在说明没有更新令牌,直接返回
/**
* 自定义Aop
*/
public class TokenAspect {
@Autowired
private ThreadLocalToken threadLocalToken;
@Pointcut("execution(public * com.yangzinan.wx.goflywxapi.controller.*.*(..))")
public void aspect(){
}
@Around("aspect()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
R r = (R) joinPoint.proceed();//获取返回值
String token = threadLocalToken.getToken(); //获取token
//如果不为空,将token添加到返回体中
if(!StrUtil.hasBlank(token)){
r.put("token",token);
threadLocalToken.remove();
}
//如果为空,说明没有可更新的令牌,直接返回响应内容
return r;
}
}
复制代码
五、完善UserRealm
上一章节,我们在写UserRealm只是大概的搭建了一个结构,没有具体的实现认证、授权的内容,我们这一章节把它完善
/**
* 自定义Realm
*/
@Component
public class UserRealm extends AuthorizingRealm {
/**
* 自定义token认证对象之后,必须重写该方法,不然会报错
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof ShiroToken;
}
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
/**
* 1.TODO:通过token获取用户id
* ---> 因为我们在ShiroToken中已经实现了getPrincipal方法,返回的是token,所以
principalCollection.getPrimaryPrincipal()获取的就是token
* ---> 我们只需要调用JWTUtil工具类获取userId即可
* 2.TODO: 通过userId获取用户权限信息
* ---> 将权限注册到SimpleAuthorizationInfo中
*/
return info;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
/**
* TODO: 认证操作
*/
return info;
}
}
复制代码
完善UserRealm,因为我们这里只使用到认证,所以我们只实现认证操作:
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("用户认证操作");
//获取令牌
String token = (String) authenticationToken.getPrincipal();
//通过jwtUtil解析用户id
int userId = jwtUtil.getUserId(token);
//查询用户信息
TbUser tbUser = userService.searchById(userId);
if(null == tbUser){
throw new LockedAccountException("账号已被锁定,请联系管理员");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(tbUser,token,getName());
return info;
}
复制代码
六、Dao Service Controller
mapper xml
<!--查询用户信息-->
<select id="searchById" parameterType="int" resultType="com.yangzinan.wx.goflywxapi.model.TbUser">
SELECT
id, open_id, nickname, photo, name, sex, tel, role, root, dept_id, status, create_time
FROM tb_user WHERE id=#{userId} AND status = 1
</select>
复制代码
Dao
@Mapper
public interface TbUserDao {
int deleteByPrimaryKey(Integer id);
int insert(TbUser record);
int insertSelective(TbUser record);
TbUser selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(TbUser record);
int updateByPrimaryKey(TbUser record);
//查询用户信息
TbUser searchById(Integer userId);
}
复制代码
service
@Service("iTbUserService")
public class TbUserServiceImpl implements ITbUserService {
@Autowired
private TbUserDao userDao;
/**
* 通过id查询用户信息
* @param userId 用户id
* @return
*/
@Override
public TbUser searchById(Integer userId) {
TbUser user = userDao.searchById(userId);
return user;
}
}
复制代码
controller
@RestController
@RequestMapping("/pub")
public class PubController {
@Autowired
private JwtUtil jwtUtil;
@GetMapping("/login")
public R login(){
return R.error(HttpStatus.HTTP_UNAUTHORIZED,"用户未登录,请登录");
}
@PostMapping("/doLogin")
public R login(Integer userId){
String token = jwtUtil.createToken(userId);
return R.ok("登录成功").put("token",token);
}
}
复制代码
因为今天中午下班回来,吃好饭已经一点多了,抽出一点时间把它写完,可能不够完善,我会在后面进行补充,谢谢!




近期评论