亲宝软件园·资讯

展开

Shiro JWT SpringBoot Shiro + JWT + SpringBoot应用代码实例详解

kuotian 人气:0

1.Shiro的简介

Apache Shiro是一种功能强大且易于使用的Java安全框架,它执行身份验证,授权,加密和会话管理,可用于保护 从命令行应用程序,移动应用程序到Web和企业应用程序等应用的安全。

从应用程序角度的来观察如何使用Shiro完成工作:

Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;

Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个Shiro应用:

1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;

2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

2.Shiro + JWT + SpringBoot

1.导入依赖

<dependency>
 <groupId>org.apache.shiro</groupId>
 <artifactId>shiro-spring</artifactId>
 <version>1.4.1</version>
</dependency>
<dependency>
 <groupId>com.auth0</groupId>
 <artifactId>java-jwt</artifactId>
 <version>3.8.2</version>
</dependency>

2.配置JWT

public class JWTUtil {
 /**
 * 校验 token是否正确
 *
 * @param token 密钥
 * @param secret 用户的密码
 * @return 是否正确
 */
 public static boolean verify(String token, String username, String secret) {
 try {
  Algorithm algorithm = Algorithm.HMAC256(secret);
  JWTVerifier verifier = JWT.require(algorithm)
   .withClaim("username", username)
   .build();
  verifier.verify(token);
  return true;
 } catch (Exception e) {
  log.info("token is invalid{}", e.getMessage());
  return false;
 }
 }

 public static String getUsername(HttpServletRequest request) {
 // 取token
 String token = request.getHeader("Authorization");
 return getUsername(UofferUtil.decryptToken(token));
 }
 /**
 * 从 token中获取用户名
 * @return token中包含的用户名
 */
 public static String getUsername(String token) {
 try {
  DecodedJWT jwt = JWT.decode(token);
  return jwt.getClaim("username").asString();
 } catch (JWTDecodeException e) {
  log.error("error:{}", e.getMessage());
  return null;
 }
 }
 
 public static Integer getUserId(HttpServletRequest request) {
 // 取token
 String token = request.getHeader("Authorization");
 return getUserId(UofferUtil.decryptToken(token));
 }
 /**
 * 从 token中获取用户ID
 * @return token中包含的ID
 */
 public static Integer getUserId(String token) {
 try {
  DecodedJWT jwt = JWT.decode(token);
  return Integer.valueOf(jwt.getSubject());
 } catch (JWTDecodeException e) {
  log.error("error:{}", e.getMessage());
  return null;
 }
 }


 /**
 * 生成 token
 * @param username 用户名
 * @param secret 用户的密码
 * @return token 加密的token
 */
 public static String sign(String username, String secret, Integer userId) {
 try {
  Map<String, Object> map = new HashMap<>();
  map.put("alg", "HS256");
  map.put("typ", "JWT");
  username = StringUtils.lowerCase(username);
  Algorithm algorithm = Algorithm.HMAC256(secret);
  return JWT.create()
   .withHeader(map)
   .withClaim("username", username)
   .withSubject(String.valueOf(userId))
   .withIssuedAt(new Date())
//   .withExpiresAt(date)
   .sign(algorithm);
 } catch (Exception e) {
  log.error("error:{}", e);
  return null;
 }
 }
}

3.配置Shiro

4.实现JWTToken

token自己已经包含了用户名等信息。

@Data
public class JWTToken implements AuthenticationToken {

 private static final long serialVersionUID = 1282057025599826155L;

 private String token;

 private String exipreAt;

 public JWTToken(String token) {
 this.token = token;
 }

 public JWTToken(String token, String exipreAt) {
 this.token = token;
 this.exipreAt = exipreAt;
 }

 @Override
 public Object getPrincipal() {
 return token;
 }

 @Override
 public Object getCredentials() {
 return token;
 }

}

5.实现Realm

自定义实现 ShiroRealm,包含认证和授权两大模块。

public class ShiroRealm extends AuthorizingRealm {

 @Resource
 private RedisUtil redisUtil;

 @Autowired
 private ISysUserService userService;

 @Autowired
 private ISysRoleService roleService;

 @Autowired
 private ISysMenuService menuService;

 // 必须重写此方法,不然Shiro会报错
 @Override
 public boolean supports(AuthenticationToken token) {
 return token instanceof JWTToken;
 }

 /**
 * 只有当需要检测用户权限的时候才会调用此方法
 * 授权模块,获取用户角色和权限。
 * @param token token
 * @return AuthorizationInfo 权限信息
 */
 @Override
 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection token) {
 Integer userId = JWTUtil.getUserId(token.toString());

 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

 // 获取用户角色集
 Set<String> roleSet = roleService.selectRolePermissionByUserId(userId);
 simpleAuthorizationInfo.setRoles(roleSet);

 // 获取用户权限集
 Set<String> permissionSet = menuService.findUserPermissionsByUserId(userId);
 simpleAuthorizationInfo.setStringPermissions(permissionSet);
 return simpleAuthorizationInfo;
 }

 /**
 * 用户认证:编写shiro判断逻辑,进行用户认证
 * @param authenticationToken 身份认证 token
 * @return AuthenticationInfo 身份认证信息
 * @throws AuthenticationException 认证相关异常
 */
 @Override
 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
 // 这里的 token是从 JWTFilter 的 executeLogin 方法传递过来的,已经经过了解密
 String token = (String) authenticationToken.getCredentials();
 String encryptToken = UofferUtil.encryptToken(token); //加密token
 String username = JWTUtil.getUsername(token); //从token中获取username
 Integer userId = JWTUtil.getUserId(token); //从token中获取userId

 // 通过redis查看token是否过期
 HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
 String ip = IPUtil.getIpAddr(request);
 String encryptTokenInRedis = redisUtil.get(Constant.RM_TOKEN_CACHE + encryptToken + StringPool.UNDERSCORE + ip);
 if (!token.equalsIgnoreCase(UofferUtil.decryptToken(encryptTokenInRedis))) {
  throw new AuthenticationException("token已经过期");
 }

 // 如果找不到,说明已经失效
 if (StringUtils.isBlank(encryptTokenInRedis)) {
  throw new AuthenticationException("token已经过期");
 }

 if (StringUtils.isBlank(username)) {
  throw new AuthenticationException("token校验不通过");
 }

 // 通过用户id查询用户信息
 SysUser user = userService.getById(userId);

 if (user == null) {
  throw new AuthenticationException("用户名或密码错误");
 }
 if (!JWTUtil.verify(token, username, user.getPassword())) {
  throw new AuthenticationException("token校验不通过");
 }
 return new SimpleAuthenticationInfo(token, token, "febs_shiro_realm");
 }
}

6.重写Filter

所有的请求都会先经过 Filter,所以我们继承官方的 BasicHttpAuthenticationFilter ,并且重写鉴权的方法。

代码的执行流程 preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin 。

@Slf4j
public class JWTFilter extends BasicHttpAuthenticationFilter {

 private static final String TOKEN = "Authorization";

 private AntPathMatcher pathMatcher = new AntPathMatcher();

 /**
 * 对跨域提供支持
 */
 @Override
 protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
 HttpServletRequest httpServletRequest = (HttpServletRequest) request;
 HttpServletResponse httpServletResponse = (HttpServletResponse) 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"));
 // 跨域时会首先发送一个 option请求,这里我们给 option请求直接返回正常状态
 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
  httpServletResponse.setStatus(HttpStatus.OK.value());
  return false;
 }
 return super.preHandle(request, response);
 }
 
 @Override
 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
 HttpServletRequest httpServletRequest = (HttpServletRequest) request;
 UofferProperties UofferProperties = SpringContextUtil.getBean(UofferProperties.class);
 // 获取免认证接口 url 
 // 在application.yml中配置/adminApi/auth/doLogin/**,/adminApi/auth/register/**, ...
 String[] anonUrl = StringUtils.splitByWholeSeparatorPreserveAllTokens(UofferProperties.getShiro().getAnonUrl(), ",");

 boolean match = false;
 for (String u : anonUrl) {
  if (pathMatcher.match(u, httpServletRequest.getRequestURI())) {
  match = true;
  }
 }
 if (match) {
  return true;
 }
 if (isLoginAttempt(request, response)) {
  return executeLogin(request, response);
 }
 return false;
 }

 /**
 * 判断用户是否想要登入。
 * 检测header里面是否包含Authorization字段即可
 */
 @Override
 protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
 HttpServletRequest req = (HttpServletRequest) request;
 String token = req.getHeader(TOKEN);
 return token != null;
 }

 @Override
 protected boolean executeLogin(ServletRequest request, ServletResponse response) {
 HttpServletRequest httpServletRequest = (HttpServletRequest) request;
 String token = httpServletRequest.getHeader(TOKEN); //得到token
 JWTToken jwtToken = new JWTToken(UofferUtil.decryptToken(token)); // 解密token
 try {
  // 提交给realm进行登入,如果错误他会抛出异常并被捕获
  getSubject(request, response).login(jwtToken);
  // 如果没有抛出异常则代表登入成功,返回true
  return true;
 } catch (Exception e) {
  log.error(e.getMessage());
  return false;
 }
 }

 @Override
 protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
 log.debug("Authentication required: sending 401 Authentication challenge response.");
 HttpServletResponse httpResponse = WebUtils.toHttp(response);
// httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
 httpResponse.setCharacterEncoding("utf-8");
 httpResponse.setContentType("application/json; charset=utf-8");
 final String message = "未认证,请在前端系统进行认证";
 final Integer status = 401;
 try (PrintWriter out = httpResponse.getWriter()) {
//  String responseJson = "{\"message\":\"" + message + "\"}";
  JSONObject responseJson = new JSONObject();
  responseJson.put("msg", message);
  responseJson.put("status", status);
  out.print(responseJson);
 } catch (IOException e) {
  log.error("sendChallenge error:", e);
 }
 return false;
 }
}

7. ShiroConfig

@Configuration
public class ShiroConfig {

 @Bean
 public ShiroRealm shiroRealm() {
 // 配置 Realm
 return new ShiroRealm();
 }
 
 // 创建DefaultWebSecurityManager
 @Bean("securityManager")
 public SecurityManager securityManager() {
 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
 // 配置 SecurityManager,并注入 shiroRealm
 securityManager.setRealm(shiroRealm());
 return securityManager;
 }
 
 // 创建ShiroFilterFactoryBean
 @Bean
 public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
 
 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
 // 设置 securityManager
 shiroFilterFactoryBean.setSecurityManager(securityManager);

 	//添加Shiro过滤器
		/**
		 * Shiro内置过滤器,可以实现权限相关的拦截器
		 * 常用的过滤器:
		 * anon: 无需认证(登录)可以访问
		 * authc: 必须认证才可以访问
		 * user: 如果使用rememberMe的功能可以直接访问
		 * perms: 该资源必须得到资源权限才可以访问
		 * role: 该资源必须得到角色权限才可以访问
		 */
 
 // 在 Shiro过滤器链上加入 自定义过滤器JWTFilter 并取名为jwt
 LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
 filters.put("jwt", new JWTFilter());
 shiroFilterFactoryBean.setFilters(filters);

 // 自定义url规则
 LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
 // 所有请求都要经过 jwt过滤器
 filterChainDefinitionMap.put("/**", "jwt");
 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
 return shiroFilterFactoryBean;
 }

 /**
 * 下面的代码是添加注解支持
 */
 @Bean
 @DependsOn({"lifecycleBeanPostProcessor"})
 public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
 // 设置代理类
 DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
 creator.setProxyTargetClass(true);

 return creator;
 }

 /**
 * 开启aop注解支持
 *
 * @param securityManager
 * @return
 */
 @Bean("authorizationAttributeSourceAdvisor")
 public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
 AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
 authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
 return authorizationAttributeSourceAdvisor;
 }

 
 // Shiro生命周期处理器
 @Bean
 public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
 return new LifecycleBeanPostProcessor();
 }

}

8.登陆

 /**
 * 登录方法
 *
 * @param username 用户名
 * @param password 密码
 * @param code 验证码
 * @param uuid 唯一标识
 * @return 结果
 */
 @PostMapping("/doLogin")
 public ResultVo login(String username, String password, String code, String uuid, HttpServletRequest request) throws UofferException {

 String verifyKey = Constant.RM_CAPTCHA_CODE_KEY + uuid;
 String captcha = redisUtil.getCacheObject(verifyKey);
 redisUtil.del(verifyKey);

 if (captcha == null) {
  return ResultVo.failed(201, "验证码失效");
 }
 if (!code.equalsIgnoreCase(captcha)) {
  return ResultVo.failed(201, "验证码错误");
 }

 username = StringUtils.lowerCase(username);
 password = MD5Util.encrypt(username, password);

 final String errorMessage = "用户名或密码错误";
 SysUser user = userManager.getUser(username);

 if (user == null) {
  return ResultVo.failed(201, errorMessage);
 }
 if (!StringUtils.equalsIgnoreCase(user.getPassword(), password)) {
  return ResultVo.failed(201, errorMessage);
 }
 if (Constant.STATUS_LOCK.equals(user.getStatus())) {
  return ResultVo.failed(201, "账号已被锁定,请联系管理员!");
 }


 Integer userId = user.getUserId();
 String ip = IPUtil.getIpAddr(request);
 String address = AddressUtil.getCityInfo(ip);
 // 更新用户登录时间
 SysUser sysUser = new SysUser();
 sysUser.setUserId(userId);
 sysUser.setLastLoginTime(new Date());
 sysUser.setLastLoginIp(ip);
 userService.updateById(sysUser);


 // 拿到token之后加密
 String sign = JWTUtil.sign(username, password, userId);
 String token = UofferUtil.encryptToken(sign);
 LocalDateTime expireTime = LocalDateTime.now().plusSeconds(properties.getShiro().getJwtTimeOut());
 String expireTimeStr = DateUtil.formatFullTime(expireTime);
 JWTToken jwtToken = new JWTToken(token, expireTimeStr);

 // 将登录日志存入日志表中
 SysLoginLog loginLog = new SysLoginLog();
 loginLog.setIp(ip);
 loginLog.setAddress(address);
 loginLog.setLoginTime(new Date());
 loginLog.setUsername(username);
 loginLog.setUserId(userId);
 loginLogService.save(loginLog);

 saveTokenToRedis(username, jwtToken, ip, address);
 JSONObject data = new JSONObject();
 data.put("Authorization", token);

 // 将用户配置及权限存入redis中
 userManager.loadOneUserRedisCache(userId);
 return ResultVo.oK(data);
 }

9.@RequiresPermissions

要求subject中必须含有bus:careerTalk:query的权限才能执行方法someMethod()。否则抛出异常AuthorizationException。

@RequiresPermissions("bus:careerTalk:query")
public void someMethod() {
}

引用:
https://www.iteye.com/blog/jinnianshilongnian-2018398
https://www.jianshu.com/p/f37f8c295057

总结

加载全部内容

相关教程
猜你喜欢
用户评论