【Shiro+SpringBoot】JWT+Shiro安全的解决用户授权认证地址过滤问题

JWT的应用场景

关于JWT是什么,可理解为使用带签名的token来做用户和权限验证,现在流行的公共开放接口用的OAuth 2.0协议基本也是类似的套路。这里只是说下选择使用jwt不用session的原因。 首先,是要支持多端,一个api要支持H5, PC和APP三个前端,如果使用session的话对app不是很友好,而且session有跨域攻击的问题。

Shiro

3个概念

  1. SecurityManager,可以理解成控制中心,所有请求最终基本上都通过它来代理转发,一般我们程序中不需要直接跟他打交道。
  2. Subject ,请求主体。比如登录用户,比如一个被授权的app。在程序中任何地方都可以通过SecurityUtils.getSubject()获取到当前的subject。subject中可以获取到Principal,这个是subject的标识,比如登陆用户的用户名或者id等,shiro不对值做限制。但是在登录和授权过程中,程序需要通过principal来识别唯一的用户。
  3. Realm,这个实在不知道怎么翻译合适。通俗一点理解就是realm可以访问安全相关数据,提供统一的数据封装来给上层做数据校验。shiro的建议是每种数据源定义一个realm,比如用户数据存在数据库可以使用JdbcRealm;存在属性配置文件可以使用PropertiesRealm。一般我们使用shiro都使用自定义的realm。 当有多个realm存在的时候,shiro在做用户校验的时候会按照定义的策略来决定认证是否通过,shiro提供的可选策略有一个成功或者所有都成功等。 一个realm对应了一个CredentialsMatcher,用来做用户提交认证信息和realm获取得用户信息做比对,shiro已经提供了常用的比如用户密码和存储的Hash后的密码的对比。

需求

搭建一个基于Shiro+JWT可以进行用户认证和授权的后台服务

编码

JWT

第一步:编写一个JWT工具类,这个类负责token的加密,解密,是否过期等问题。

public class JWTUtil {
    // 过期时间 24 小时
    private static final long EXPIRE_TIME = 60 * 24 * 60 * 1000;


    /**
     * 生成 token
     */
    public static String createToken(String username,String password) {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(password);
            // 附带username信息
            return JWT.create()
                    .withClaim("username", username)
                    //到期时间
                    .withExpiresAt(date)
                    //创建一个新的JWT,并使用给定的算法进行标记
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

    /**
     * 校验 token 是否正确
     */
    public static boolean verify(String token, String username,String password) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(password);
            //在token中附带了username信息
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            //验证 token
            verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 获得token中的信息,无需secret解密也能获得
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}

第二步:编写Filter,控制是否登入,跨域请求开启的功能

public class JWTFilter extends BasicHttpAuthenticationFilter {
			@Override
			protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
				if(isLoginAttempt(request, response)) {
					try {
						executeLogin(request, response);
						return true;
					} catch (Exception e) {
						responseError(response, e.getMessage());
						
					}
				}
				return true;
			}
			
			@Override
			protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
			HttpServletRequest requests=(HttpServletRequest) request;
			String token = requests.getHeader("Authorization");
			return token!=null;
			}
			
			@Override
			protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
				 HttpServletRequest httpServletRequest = (HttpServletRequest) request;
				 String token = httpServletRequest.getHeader("Authorization");
			     JWTToken jwtToken = new JWTToken(token);
				 // 提交给realm进行登入,如果错误它会抛出异常并被捕获
				  getSubject(request, response).login(jwtToken);
				return true;
			}
			@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);
			}
			
			private void responseError(ServletResponse response, String message) {
				HttpServletResponse responses=(HttpServletResponse) response;
				message=URLEncoder.encode(message);
				try {
					responses.sendRedirect("/unauthorized/" + message);
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		
}


第三步:重新包装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;
    }
}

Shiro

第一步:编写Shiro的配置类,定义jwt过滤器,访问策略,securitymanager,realm等。

@Configuration
public class ShiroConfig {
	@Bean
	public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
		ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
	     Map<String, Filter> filterMap = new LinkedHashMap<>();
	        //设置我们自定义的JWT过滤器
	     filterMap.put("jwt", new JWTFilter());
	     
	     shiroFilterFactoryBean.setFilters(filterMap);
	     shiroFilterFactoryBean.setSecurityManager(manager);
		shiroFilterFactoryBean.setLoginUrl("/login");
		shiroFilterFactoryBean.setUnauthorizedUrl("/notRule");
		Map<String,String> filterChainDefinitionMap=new LinkedHashMap<>();
		filterChainDefinitionMap.put("/**","jwt");
		filterChainDefinitionMap.put("/login","anon");
		filterChainDefinitionMap.put("/","anon");
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
		
		return shiroFilterFactoryBean;
	}
 
	   @Bean("securityManager")
	   public SecurityManager securityManager() {
	       DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
	       // 设置realm.
	       securityManager.setRealm(customRealm());
	       /*
	         * 关闭shiro自带的session
	         */
	        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
	        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
	        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
	        
	        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
	        securityManager.setSubjectDAO(subjectDAO);
	       return securityManager;
	   }
	   
	    @Bean
	    public CustomRealm customRealm() {
	        return new CustomRealm();
	    }
	    @Bean
	    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
	        return new LifecycleBeanPostProcessor();
	    }
	    @Bean
	    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
	        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
	        advisor.setSecurityManager(securityManager);
	        return advisor;
	    }
	    @Bean
	    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
	        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
	        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
	        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
	        return defaultAdvisorAutoProxyCreator;
	    }

}


第二步:编写Realm实现认证和授权

public class CustomRealm extends AuthorizingRealm{
	@Autowired
	UserRepository rp;

	/**
	 * 授权
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		String username = JWTUtil.getUsername(principals.toString());
		SimpleAuthorizationInfo sa=new SimpleAuthorizationInfo();
		String role=rp.findByusername(username).getRole();
		HashSet<String> set = new HashSet<String>();
		set.add(role);
		sa.setRoles(set);
		return sa;
	}
	/**
	 * 认证
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		String token = (String) authenticationToken.getCredentials();
		 // 解密获得username,用于和数据库进行对比
        String username = JWTUtil.getUsername(token);
    	String password = rp.findByusername(username).getPassword();
        if (username == null || !JWTUtil.verify(token, username,password)) {
            throw new AuthenticationException("token认证失败!");
        }
	   if (null == password) {
            throw new AccountException("用户名不正确");
        } else if (!password.equals(new String((char[]) authenticationToken.getCredentials()))) {
            throw new AccountException("密码不正确");
        }
		SimpleAuthenticationInfo simpleinfo=new SimpleAuthenticationInfo(token, token,"MyRealm");
		return simpleinfo;
	}   
	@Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }
    

}

总结

在ShiroConfig中配置JWTFilter,SecurityManager[Realm],访问策略。JWTFilter拦截请求并判断是否合法,若合法调用传递给Realm进行认证和授权。 PS:JWT的token其实是通过password对username进行加密签名形成的一串字符串代码。在解析的时候也需要查询数据库获得密码进行解密

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值