springboot+vue前后端分离整合shiro+jwt

0.实现逻辑

springboot+vue前后端分离整合shiro+jwt_第1张图片
用户登录:
经过shiro中已配置的JwtFilter,执行onAccessDenied方法,判断没有jwt,放行,访问登录接口,进行查询数据库进行用户名密码校验,生成jwt并响应给客户端,返回ResultBody,至此登录执行完毕。

用户未登录访问其他接口:
经过shiro中已配置的JwtFilter,执行onAccessDenied方法,判断没有jwt,放行,访问公共接口成功,访问受限接口失败。

用户已登录访问其他接口:
用户携带jwt,经过shiro中已配置的JwtFilter,执行onAccessDenied方法,判断有jwt,判断jwt是否过期,过期抛出异常,否则交给shiro执行登录,由UserRealm完成用户校验,校验通过则认证成功,即可访问其他接口。

1.导入依赖

导入shiro-redis的starter包:还有jwt的工具包

<dependency>
    <groupId>org.crazycakegroupId>
    <artifactId>shiro-redis-spring-boot-starterartifactId>
    <version>3.2.1version>
dependency>


<dependency>
    <groupId>io.jsonwebtokengroupId>
    <artifactId>jjwtartifactId>
    <version>0.9.1version>
dependency>

2.JwtToken

shiro默认supports的是UsernamePasswordToken,而我们现在采用了jwt的方式,所以这里我们自定义一个JwtToken,来完成shiro的supports方法。

public class JwtToken implements AuthenticationToken {
	
	private String jwt;
	
	public JwtToken(String jwt) {
		this.jwt = jwt;
	}
	/**
	 * -这里getPrincipal和getCredentials统一返回jwt串
	 * -因为在后面的自定义realm中我们封装进SimpleAuthenticationInfo的principal是vo对象,credentials是jwt串
	 */
	@Override
	public Object getPrincipal() {
		return jwt;
	}
	
	@Override
	public Object getCredentials() {
		return jwt;
	}
}

3.JwtFilter

这里我们继承的是Shiro内置的AuthenticatingFilter,一个可以内置了可以自动登录方法的的过滤器,继承BasicHttpAuthenticationFilter也是可以的。

@Component
public class JwtFilter extends AuthenticatingFilter {
	
	@Autowired
	private JwtUtils jwtUtils;
	
	/**
	 * -实现登录,生成自定义支持的jwt_token
	 */
	@Override
	protected AuthenticationToken createToken(ServletRequest req, ServletResponse res) throws Exception {
		
		HttpServletRequest request = (HttpServletRequest) req;
		//从请求头中获取用户携带的jwt
		String jwt = request.getHeader("Authorization");
		
		if(StringUtils.isEmpty(jwt)) {
			return null;
		}
		//作为形参传到自定义realm中的doGetAuthenticationInfo方法
		return new JwtToken(jwt);
	}
	
	/**
	 * -不是登录请求的时候,isAccessAllowed方法返回false,会执行该方法
	 */
	@Override
	protected boolean onAccessDenied(ServletRequest req, ServletResponse res) throws Exception {
		
		HttpServletRequest request = (HttpServletRequest) req;
		//从请求头中获取用户携带的token
		String jwt = request.getHeader("Authorization");
		
		//这里我们不对没有jwt的用户拦截,没有携带jwt,说明是游客身份访问,则不需要交给shiro,而是交给控制器注解拦截
		if(StringUtils.isEmpty(jwt)) {
			return true;
		}else {
			//校验jwt是否过期
			Claims claims = jwtUtils.getClaimsByJwt(jwt);
			if(ObjectUtils.isEmpty(claims)||jwtUtils.isExpired(claims.getExpiration())) {
				throw new ExpiredCredentialsException("token已过期,请重新登录");
			}
			//交给shiro执行登录,其实每次用户每次访问资源都相当于作了自动登录
			return executeLogin(req,res);
		}
	}
	
	/**
	 * -登录方法异常405时执行该方法
	 */
	@Override
	protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest req,ServletResponse res){
		
		HttpServletResponse response = (HttpServletResponse) res;
		
		Throwable throwable = e.getCause() == null ? e : e.getCause();
		
		//封装错误信息的结果体
		ResultBody resultBody = ResultBody.error(405,throwable.getMessage());
		
		try {
			String json = new ObjectMapper().writeValueAsString(resultBody);
			//把结果体写入响应流
			response.getWriter().write(json);
		} catch (Exception exception) {
			exception.printStackTrace();
		}
		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);
	}
}

4.JwtUtils

4.1 在application.properties做如下配置

# jwt配置信息
jwt.config.secret=liangzhankai
jwt.config.header=Authorization
jwt.config.expire=604800

4.2 工具类开发

@Data
@Component
@ConfigurationProperties(prefix = "jwt.config")
public class JwtUtils {
	
	private String secret;
    private long expire;
    private String header;
    
    /**
     * -根据用户id生成jwt_token
     */
    public String generateToken(String userId) {
    	//当前时间
		Date nowDate = new Date();
		//过期时间,7天
		Date expireDate = new Date(nowDate.getTime() + expire * 1000);
		
		return Jwts.builder()
		           .setHeaderParam("typ", "JWT")	//header,设置类型为JWT
		           .setSubject(userId) 	//payload标准中注册的声明,存放用户id,作为用户的唯一标志。
		           .setIssuedAt(nowDate)	//payload标准中注册的声明,设置jwt发行时间
		           .setExpiration(expireDate)	//payload标准中注册的声明,设置过期时间
		           .signWith(SignatureAlgorithm.HS512, secret)	//header,设置加密算法和密钥
		           .compact();		//压缩为xxx.xxx.xxx这样的jwt串
    }
    
    /**
     * -从jwt中获得Claims,Claims在JWT原始内容中是一个JSON格式的字符串,其中单个Claim是K-V结构
     * @param token
     * @return
     */
    public Claims getClaimsByJwt(String jwt) {
            try {
				return Jwts.parser()	//获得默认jwt解析器,注意:如果jwt已经过期了,这里会抛出jwt过期异常。
				           .setSigningKey(secret)	//设置密钥解密
				           .parseClaimsJws(jwt)	//设置需要解析的jwt
				           .getBody();
			} catch (ExpiredJwtException e) {
				e.printStackTrace();
			} catch (UnsupportedJwtException e) {
				e.printStackTrace();
			} catch (MalformedJwtException e) {
				e.printStackTrace();
			} catch (SignatureException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			}
            return null;
    }

    /**
     * token是否过期
     * @return  true:过期
     */
    public boolean isExpired(Date expiration) {
    	//如果过期时间在当前时间之前,说明已经过期
        return expiration.before(new Date());
    }
}

5.1 UserVO

@Data
@Accessors(chain=true)
public class UserVO {
	private String id;
	
	@NotBlank(message="用户名不能为空")
	private String username;
	
	@NotBlank
	@Email(message="邮箱格式错误")
	private String email;
	
	private String avatar;
}

5.2 UserDTO

@Data
@Accessors(chain = true)
public class UserDTO implements Serializable{
	
	@NotBlank(message="用户名不能为空")
	private String username;
	
	@NotBlank(message="密码不能为空")
	private String password;
}

其实这里可以不用VO,直接传输DTO也可
注意:这里使用了实体校验的注解,需要引入依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-validationartifactId>
dependency>

6.UserRealm

@Component
public class AccountRealm extends AuthorizingRealm {
	
	@Autowired
	private JwtUtils jwtUtils;
	
	@Autowired
	private UserService userService;
	
	/**
	 * -查询数据库,将获取到用户的角色及权限信息返回
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		//获取主身份信息,即我们在SimpleAuthenticationInfo中封装的 userVO
		UserVO userVO = (UserVO) principals.getPrimaryPrincipal();
		
		//获取数据库中的 user
		User userDB = userService.findByUsername(userVO.getUsername());
		
		//如果角色集合的第一个元素不为空
		if(!ObjectUtils.isEmpty(roles.get(0))) {
		
			//创建权限信息对象
			SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
		
			//遍历roles集合,把角色名依次添加进权限信息对象
			roles.forEach(role->{
				simpleAuthorizationInfo.addRole(role.getName());
				
				//获取角色对应的权限字符串集合
				List<Permission> permissions = permissionService.listPermissionByRoleId(role.getId());
				System.out.println(permissions+"...................");
				//遍历权限字符串集合,添加进权限信息对象
				permissions.forEach(permission->{
					simpleAuthorizationInfo.addStringPermission(permission.getName());
				});
			});
			return simpleAuthorizationInfo;
		}
		return null;
	}
	
	/**
	 * -根据token获得身份信息查询数据库作身份认证
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		
		//把token强转成自定义的jwt_token
		JwtToken jwtToken = (JwtToken) token;
		
		//取出jwt串
		String jwt = (String) jwtToken.getPrincipal();
		
		//使用jwt工具类获得claims中的claim的userId
		String userId = jwtUtils.getClaimsByJwt(jwt).getSubject();
		//查询数据库获取用户
		User userDB = userService.findById(userId);
		//判断用户异常状态,抛出异常
		if(userDB==null) {
			throw new UnknownAccountException("用户不存在!");
		}
		if(userDB.getStatus()==-1) {
			throw new LockedAccountException("用户已被禁用!");
		}
		
		//把账户非敏感信息封装成vo,作为 principal
		UserVO userVO = new UserVO()
				.setId(userDB.getId())
				.setUsername(userDB.getUsername())
				.setAvatar(userDB.getAvatar())
				.setEmail(userDB.getEmail());
		
		/**
		 * -返回认证信息,参数1:principal身份信息  参数2:credentials凭证信息  参数3:realmName
		 * -shiro最后从jwtToken和SimpleAuthenticationInfo实例中分别调用各自的getCredentials()拿到credentials进行校验
		 */
		return new SimpleAuthenticationInfo(userVO,jwtToken.getCredentials(),this.getName());
	}
	
	/**
	 * -指定当前realm只处理自定义的jwt_token
	 */
	@Override
	public boolean supports(AuthenticationToken token) {
		return token instanceof JwtToken;
	}
}

7.编写ShiroConfig

@Configuration
public class ShiroConfig {
	
	@Autowired
	private JwtFilter jwtFilter;
	
	/**
	 * -引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享
	 * @param redisSessionDAO
	 * @return
	 */
	@Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }
	
	/**
	 * -重写了SessionManager和DefaultWebSecurityManager,
	 * -同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,需要设置为false,
	 * -这样用户就不再能通过session方式登录shiro,后面将采用jwt凭证登录。
	 * @param accountRealm,自定义realm
	 * @param sessionManager
	 * @param redisCacheManager
	 * @return
	 */
	@Bean
    public DefaultWebSecurityManager securityManager(Realm  realm,SessionManager sessionManager,RedisCacheManager redisCacheManager){
        //创建安全管理器
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);
		//在安全管理器中注入会话管理器和redis缓存管理器
        securityManager.setSessionManager(sessionManager);
        securityManager.setCacheManager(redisCacheManager);
        
        //关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    } 
	
	/**
	 * -设置受限资源与过滤器映射的对应关系
	 * @return
	 */
	@Bean
	public ShiroFilterChainDefinition shiroFilterChainDefinition() {
		DefaultShiroFilterChainDefinition filterChainDefinition = new DefaultShiroFilterChainDefinition();
		//创建受限资源映射
	    Map<String, String> filterChainMap = new LinkedHashMap<>();
	    //所有资源都要经过名为jwt映射的过滤器
	    filterChainMap.put("/**", "jwt"); 
	    //把受限资源映射加入过滤器链定义
	    filterChainDefinition.addPathDefinitions(filterChainMap);
	    return filterChainDefinition;
	}
	
	/**
	 * -过滤器工厂
	 * @param securityManager
	 * @param shiroFilterChainDefinition
	 * @return
	 */
	@Bean("shiroFilterFactoryBean")
	public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
        //创建shiro过滤器工厂
		ShiroFilterFactoryBean shiroFilterFactory = new ShiroFilterFactoryBean();
		
		//在shiro中注入安全管理器
        shiroFilterFactory.setSecurityManager(securityManager);
        
        //创建jwt映射的jwtFilter并加入shiroFilterFactory
        Map<String, Filter> filters = new HashMap<>();
		filters.put("jwt", jwtFilter);
        shiroFilterFactory.setFilters(filters);
        
        //获得受限资源与过滤器映射关系,如 /**:"jwt"
        Map<String, String> filterChainDefinition = shiroFilterChainDefinition.getFilterChainMap();
        
        //把受限资源与过滤器映射的对应关系注入shiro过滤器工厂
        shiroFilterFactory.setFilterChainDefinitionMap(filterChainDefinition);
        return shiroFilterFactory;
	}
}

上面的配置,我们主要做了这几件事:

1.引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。

2.重写了SessionManager和DefaultWebSecurityManager,同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,我们需要设置为false,这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。

3.在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径,而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication,这样控制权限访问。

8.全局配置,解决应用接口跨域问题

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("*");
    }
}

9.登录接口

@PostMapping("/login")
public ResultBody login(@RequestBody @Validated UserDTO userDTO,HttpServletResponse response) {
		
	//根据传过来的用户名查询数据库
	User userDB = userService.findByUsername(userDTO.getUsername());
		
	//校验用户
	if(userDB!=null) {
		//根据用户名生成盐
		ByteSource salt = ByteSource.Util.bytes(userDTO.getUsername());
		//对密码使用 md5散列1024次+盐 加密
		Md5Hash md5Hash = new Md5Hash(userDTO.getPassword(),salt,1024);
		//比较密码
		if(!md5Hash.toHex().equals(userDB.getPassword())) {
			//抛出密码不正确异常
			throw new CredentialsException("密码错误!");
		}
	}else {
		//抛出用户不存在异常
		throw new UnknownAccountException("用户不存在!");
	}
		
	//生成jwt
	String jwt = jwtUtils.generateToken(userDB.getId());
		
	//把jwt封装在响应头返回给客户端
	response.setHeader("Authorization", jwt);
	response.setHeader("Access-control-Expose-Headers", "Authorization");
		
	//封装vo对象传给前端
	UserVO userVO = new UserVO()
					.setId(userDB.getId())
					.setUsername(userDB.getUsername())
					.setAvatar(userDB.getAvatar())
					.setEmail(userDB.getEmail());
		
	return ResultBody.success(CommonEnum.SUCCESS,userVO);
}

10.前端vue路由拦截

10.1 编写store状态管理

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state:{
    token:localStorage.getItem('token'),
    userInfo:JSON.parse(localStorage.getItem('userInfo'))
  },
  mutations:{
    //对state定义的变量进行操作
    SET_TOKEN:(state,token)=>{
      state.token = token;
      localStorage.setItem('token',token);
    },
    SET_USERINFO:(state,userInfo)=>{
      state.userInfo = userInfo;
      localStorage.setItem('userInfo',JSON.stringify(userInfo));
    },
    REMOVE_TOKEN:(state,token)=>{
      state.token='';
      localStorage.removeItem('token');
    },
    REMOVE_USERINFO:(state,userInfo)=>{
      state.userInfo={};
      localStorage.removeItem('userInfo');
    }
  },
  getters:{
    //获取state里的变量
    getUserInfo:state=>{
      return state.userInfo
    },
    getToken:state=>{
      return state.token;
    }
  },
  actions:{
  },
  modules:{
  }
})

10.2 配置axios拦截

import axios from 'axios'
import ElementUI from 'element-ui'
import router from './router'
import store from './store/index.js'

// 请求url的前缀
axios.defaults.baseURL = 'http://localhost:8081'

// 前置拦截
axios.interceptors.request.use(config=>{
  //携带token
  let token = localStorage.getItem('token');
  if(token){
    //每次请求都携带token
    config.headers['Authorization'] = token;
  }
  return config;
})

//后置拦截
axios.interceptors.response.use(response=>{

  let res = response.data;

  if(res.code == 400){
    //出现shiro异常,跳转到登录页面
    ElementUI.Message.error(res.msg);
    router.push({name:'Login'});
    //阻止后续登录成功逻辑的代码
    return Promise.reject(res.msg);
  }else if(res.code >= 401 && res.code < 500){
    ElementUI.Message.error(res.msg);
    //阻止后续登录成功逻辑的代码
    return Promise.reject(res.msg);
  }else{
    //正常返回
    return response;
  }
})

10.3 编写路由权限拦截

import router from './router'
import ElementUI from 'element-ui'

// 路由判断登录,根据路由器配置文件的参数
router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requireAuth)) { //需要权限的路由
      const token = localStorage.getItem("token");
      if (!token) { //没有token
        ElementUI.Message.error('权限不足,请先登录');
        next({  //跳转到登录页
          path:'/login'
        })
      }
  } else { //不需要权限的路由
    next();
  }
})

你可能感兴趣的:(笔记,Java,jwt,shiro,spring,vue)