昨天我发了一个单点登录版本的验证博客,到今天早上我再研究了一下,发现了一些问题:
所以,下面就是介绍与GateWay结合的方式去认证.
这里我为什么没提OAuth,因为没有OAuth也是可以进行鉴权的,但是这种方式你必须使用一个中间件,去保存需要鉴权的路径,哪些角色可以访问哪些路径你必须保存下来,因为我们在单个微服务上是没有鉴权操作了的,而是在GateWay网关里鉴权,所以无法在每个方法上加上注解的方式去鉴权,就必须得保存每个路径是哪个角色可以访问的.
所以这里就必须由一个基本的关系,做这个之前必须得清楚:

一个用户可以由多个角色,一个角色又可以有多个权限,所以这个用户包含其所拥有角色的所有权限.
怎么给用户赋予角色,给角色赋予权限,是数据库的事情了,假如这里已经有一些数据,反正你就记得是这个结构就行.
那么下面的鉴权流程可以是
前端有一个请求,直接打到了认证中心,这个时候,先来到AbstractAuthenticationProcessingFilter中的doFilter方法进行请求过滤,通常可以实现自定义过滤器,过滤成功,尝试鉴权attemptAuthentication,这个方法是由其子类UsernamePasswordAuthenticationFilter实现的,通常可以实现自定义鉴权,然后来到UserDetailsService的loadUserByUsername方法,这个方法经常用来做自定义登录逻辑,授权成功后执行successfulAuthentication方法,这个方法在doFilter方法里被调用,底层会调用核心方法onAuthenticationSuccess,该方法用来实现自定义授权成功逻辑,相反,就有一个自定义授权失败逻辑AuthenticationFailureHandler接口中的onAuthenticationFailure方法
这里多提一嘴,如果是要自定义鉴权逻辑去代替系统的逻辑,一般都是通过自定义才能实现多彩的鉴权方案,至于有些人为什么自定义鉴权逻辑没有生效,那是因为你没有在配置方法configure(HttpSecurity http)中加入这些个逻辑.
下面开始上代码了,先直接上代码,继续往下我会给出相关代码的解释
@Component
public class SheepUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
System.out.println("函数进来了");
if( !"admin".equals(s) )
throw new UsernameNotFoundException("用户" + s + "不存在" );
//TODO
//根据用户名查询对应角色,然后根据角色查询对应所能访问路径
//将这些路径包装成集合传过去
//获取你直接查询到了角色,先将角色传过去,我这里是先将角色传过去
int id=0;
return new User( s+"-"+id, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM"));
}
}
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
User principal = (User) authentication.getPrincipal();
String username = principal.getUsername();
String[] users = username.split("-");
// TODO 得到该用户全部角色
//Collection<GrantedAuthority> authorities = principal.getAuthorities();
//TODO 查询数据库得到该角色所能访问的全部路径
//模拟:
String[] s1=new String[]{"/login","/register","/serviceedu/front/listTeacher"};
//将权限路径封装到redis中
redisUtils.setCollectionSet(users[0],s1,24, TimeUnit.HOURS);
String s= redisUtils.get("fromUrl");
if (s==null)
s="/";
String jwtToken = JwtUtils.getJwtToken(users[1], users[0]);
Msg msg = Msg.success().data("username", users[0]).data("fromUrl",s).data("token",jwtToken);
redisUtils.del("fromUrl");
httpServletResponse.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回给前台
httpServletResponse.getWriter().write(JSON.toJSONString(msg));
}
}
@Component
public class FailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Msg fail = Msg.fail().data("message", "登录失败或权限不足");
httpServletResponse.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回给前台
//这种方式很常用,后面的代码都有很多,唯一能反馈给前端json格式的消息
httpServletResponse.getWriter().write(JSON.toJSONString(fail));
}
}
@Component
public class CustomLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 将子系统的cookie删掉
//建议将token也删除,直接写个controller接口就可以了,可以在前端调用/logout的同时调用删除token接口
Cookie[] cookies = request.getCookies();
if(cookies != null && cookies.length>0){
for (Cookie cookie : cookies){
cookie.setMaxAge(0);
cookie.setPath("/");
response.addCookie(cookie);
}
}
super.handle(request, response, authentication);
}
}
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
SuccessHandler successHandler;
@Autowired
FailHandler failHandler;
@Autowired
private CustomLogoutSuccessHandler customLogoutSuccessHandler;
//@Autowired
//CustomizeAuthenticationEntryPoint customizeAuthenticationEntryPoint;
// @Autowired
// MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter;
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setHideUserNotFoundExceptions(false);
return authenticationProvider;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// .usernameParameter("mobile")
// .passwordParameter("password")
//自定义登录页面,这个页面是一个controller路径
//我们只需要在对应的controller中重定向到你前端的页面就可以了
.loginPage("/unLogin")
//登录处理逻辑路径,/login代表用系统的处理逻辑
//但是我们重写了用户逻辑,所以会走到重写的用户逻辑里
.loginProcessingUrl("/login")
//自定义鉴权成功处理
.successHandler(successHandler)
//自定义鉴权失败处理
.failureHandler(failHandler)
.permitAll()
.and()
.logout()
//自定义登录成功处理
.logoutSuccessHandler(customLogoutSuccessHandler)
// 无效会话
.invalidateHttpSession(true)
// 清除身份验证
.clearAuthentication(true)
.and().csrf().disable();
//异常处理(权限拒绝、登录失效等)
// .exceptionHandling()
// .authenticationEntryPoint(customizeAuthenticationEntryPoint);
http.authorizeRequests()
.antMatchers(
"/oauth/**",
"/login/**",
"/unLogin",
"/logout/**",
"/uac/oauth/token",
"http://localhost:3000/login",
"http://localhost:8085/uac/login"
).permitAll().anyRequest().authenticated();
//将@bean注入的鉴权器加入到配置当中
//http.addFilterAt(myAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
//如果自定义鉴权器,也要做相关配置,不然不走你的鉴权器
// @Bean
// MyUsernamePasswordAuthenticationFilter myAuthenticationFilter() throws Exception {
// MyUsernamePasswordAuthenticationFilter filter = new MyUsernamePasswordAuthenticationFilter();
// filter.setAuthenticationManager(authenticationManagerBean());
// filter.setAuthenticationSuccessHandler(successHandler);
// filter.setAuthenticationFailureHandler(failHandler);
// return filter;
// }
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}
认证中心这样的就完成了,有人会问,怎么没有OAtuh的配置?注意我这里是用SpringSecurity+GateWay方式去认证,用不到OAuth.
这也是为什么你会看到我用了一个redis去保存用户跟权限这两者的关系,我这里为什么不保存角色,因为没那个必要,虽然一个用户有多个角色,但是这些角色下的权限都属于用户的,而前端传过来的是路径,所以这里我们只是存储用户跟路径权限之间的关系,到时候就可以直接判断该用户是否可以访问该路径了.
那么还有一个小问题就是,你会发现我并没有利用认证中心底层给我生成的token,这个token很难利用上,要利用的话估计要实现自定义过滤器,反正我拿不到.
但是我想到了一个绝妙且笨的方法,就是在鉴权成功的自定义处理方法中,可以直接用RestTemplate发送一个/oauth/token的请求,这样子不就直接拿到token值了吗?然后将其反馈给前端,也不用我们自己去生成这个token
@Component("a")
@Order(2)
//设置执行优先级,在 全局权限认证过滤器 之前执行
public class AuthenticationFilter implements GlobalFilter, InitializingBean {
@Autowired
private RestTemplate restTemplate;
@Autowired
RedisUtils redisUtils;
private static Set<String> shouldSkipUrl = new LinkedHashSet<>();
@Override
public void afterPropertiesSet() throws Exception {
// 在类被初始化完成时,把不拦截认证的请求放入集合
shouldSkipUrl.add("/uac");
shouldSkipUrl.add("/serviceedu/front/listTeacher");
}
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取request请求
String requestPath = exchange.getRequest().getURI().getPath();
//如果请求url不需要认证,直接跳过
if(shouldSkip(requestPath)) {
return chain.filter(exchange);
}
//获取Authorization请求头
String token = exchange.getRequest().getHeaders().getFirst("token");
//Authorization请求头为空,抛异常
if(StringUtils.isEmpty(token)) {
return out(exchange.getResponse());
}
redisUtils.set("fromUrl",requestPath,10, TimeUnit.MINUTES);
if (!JwtUtils.checkToken(token)){
return out(exchange.getResponse());
}
Claims memberClaims = JwtUtils.getMemberClaims(token);
if (memberClaims==null)
return out(exchange.getResponse());
String nickname = (String) memberClaims.get("nickname");
Set<String> set = redisUtils.getSet(nickname);
if (!hasPermisson(set,requestPath)){
exchange.getResponse().setStatusCode(HttpStatus.NON_AUTHORITATIVE_INFORMATION);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
private Mono<Void> out(ServerHttpResponse response) {
JsonObject message = new JsonObject();
message.addProperty("success", false);
message.addProperty("code", 20001);
message.addProperty("data", "鉴权失败");
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
//response.setStatusCode(HttpStatus.UNAUTHORIZED);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
private boolean hasPermisson(Set<String> set,String path){
for (String s :
set) {
if (path.contains(s))
return true;
}
return false;
}
private boolean shouldSkip(String reqPath) {
for(String skipPath:shouldSkipUrl) {
if(reqPath.contains(skipPath)) {
return true;
}
}
return false;
}
}
这样子,验证失败了,我们也能传给前端一个json格式的数据,让前端做出反应,验证成功后,就可以放心大胆的去让用户浏览该路径了,效率很高,因为只有第一次需要请求认证中心鉴权,往后直接拿token解析取出redis中的映射路径去鉴权.
其实这里我有个问题的: 就是如果在GateWay网关鉴权,那有用户绕过了网关去访问某个微服务呢?这又该怎么办,那岂不是所有的权限都暴露出来,但是我又想到了其实绕过去极为困难.
但我们不得不承认的事情是,凡事都有两面性,你在网关鉴权了,效率上是高,但是加重了网关的负担,提高了单点故障的风险,如果网关挂了怎么办?通常只能搞成网关"集群"了.
这个其实可以省去redis,就是利用RestTemplate来进行鉴权,也是在网关处,加快性能:
@Component("a")
@Order(2)
//设置执行优先级,在 全局权限认证过滤器 之前执行
public class AuthenticationFilter implements GlobalFilter, InitializingBean {
@Autowired
private RestTemplate restTemplate;
@Autowired
RedisUtils redisUtils;
private static Set<String> shouldSkipUrl = new LinkedHashSet<>();
@Override
public void afterPropertiesSet() throws Exception {
// 在类被初始化完成时,把不拦截认证的请求放入集合
shouldSkipUrl.add("/uac");
shouldSkipUrl.add("/serviceedu/front/listTeacher");
}
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取request请求
String requestPath = exchange.getRequest().getURI().getPath();
//如果请求url不需要认证,直接跳过
if(shouldSkip(requestPath)) {
return chain.filter(exchange);
}
//获取Authorization请求头
String token = exchange.getRequest().getHeaders().getFirst("token");
//Authorization请求头为空,抛异常
if(StringUtils.isEmpty(token)) {
return out(exchange.getResponse());
}
TokenInfo tokenInfo=null;
try {
//往授权服务发http请求 /oauth/check_token 并封装返回结果!
tokenInfo = getTokenInfo(authHeader);
}catch (Exception e) {
throw new RuntimeException("校验令牌异常");
}
if(!hasPremisson(tokenInfor,requestPath){
return out(exchange.getResponse());
}
return chain.filter(exchange);
}
private Mono<Void> out(ServerHttpResponse response) {
JsonObject message = new JsonObject();
message.addProperty("success", false);
message.addProperty("code", 20001);
message.addProperty("data", "鉴权失败");
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
//response.setStatusCode(HttpStatus.UNAUTHORIZED);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
private boolean hasPremisson(TokenInfo tokenInfo,String currentUrl) {
boolean hasPremisson = false;
//登录用户所拥有的请求url权限集合
List<String> premessionList = Arrays.asList(tokenInfo.getAuthorities());
//与当前请求url,看是否有对应的访问权限
for (String url: premessionList) {
if(currentUrl.contains(url)) {
hasPremisson = true;
break;
}
}
//如果没有,抛异常
if(!hasPremisson){
throw new RuntimeException("没有权限");
}
return hasPremisson;
}
private boolean shouldSkip(String reqPath) {
for(String skipPath:shouldSkipUrl) {
if(reqPath.contains(skipPath)) {
return true;
}
}
return false;
}
private TokenInfo getTokenInfo(String authHeader) {
// 往授权服务发请求 /oauth/check_token
// 获取token的值
String token = StringUtils.substringAfter(authHeader, "bearer ");
//组装请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
//必须设置 basicAuth为对应的 clienId、 clientSecret
headers.setBasicAuth("admin", "123456");
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("token", token);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);
//往授权服务发http请求 /oauth/check_token
ResponseEntity<TokenInfo> response = restTemplate.exchange("http://localhost:8085/uac/oauth/check_token", HttpMethod.POST, entity, TokenInfo.class);
//获取响应结果 TokenInfo
return response.getBody();
}
}
网上的鉴权方式差不多都是这个方式,直接发送RestTemplate风格的请求去验证token是否正确,然后返回一个权限列表,但在这里需要注意的一点是:
你这个token必须从前端过来是认证中心生成的那个,因为你要发送check_token请求是去认证中心底层/oauth/check_token路径验证的,那它肯定是拿自己生成那个token验证,而且你必须将这个token反馈给前端,就需要知道如何在认证中心的认证成功方法里将底层的token反馈给前端,这样这边网关层才能发送过来,如果你需要验证自己生成的token,可以在认证中心自定义/oauth/token,所以这个方法很麻烦.
其实这里我提出一个疑惑,我在网上看的时候,有人用这种方法重写了两个全局过滤器,有些蒙蔽,为啥要这样做,不是多余了吗?他是将两个一样的过滤器分不同前后加载到ioc容器中,然后在前一个过滤器采用setAttribute的方式放在里面,之后在后面的过滤器里取出来,???各位网友可以说说,是不是多余了,用了两个一模一样的过滤器,直接在第一个过滤器里查出来后验证不就行了吗?
上面的GateWay网关中,还可以这样优化:
因为你从验证中心发送请求获取数据的,所以你必须得每次都请求一次,这样子效率不太好,所以你可以将第一次请求的数据缓存下来,网上的方式都是放在请求头里,一般不要这样做,因为请求头我完全可以在前端制造,这样子就危险了,而是放在redis里,在请求之前先查缓存,如果没有缓存再请求,有缓存就直接拿出缓存鉴权.这样子是不是发现又回到了第一个方式的鉴权?所以我没有使用第二种方式.
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende
给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最
目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称
@作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors 1、什么是behaviors 2、behaviors的工作方式 3、创建behavior 4、导入并使用behavior 5、behavior中所有可用的节点 6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors 1、什么是behaviorsbehaviors是小程序中,用于实现
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
ES一、简介1、ElasticStackES技术栈:ElasticSearch:存数据+搜索;QL;Kibana:Web可视化平台,分析。LogStash:日志收集,Log4j:产生日志;log.info(xxx)。。。。使用场景:metrics:指标监控…2、基本概念Index(索引)动词:保存(插入)名词:类似MySQL数据库,给数据Type(类型)已废弃,以前类似MySQL的表现在用索引对数据分类Document(文档)真正要保存的一个JSON数据{name:"tcx"}二、入门实战{"name":"DESKTOP-1TSVGKG","cluster_name":"elasticsear
了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl
我需要使用ActiveMerchant库在我们的一个Rails应用程序中设置支付解决方案。尽管这个问题非常主观,但人们对主要网关(BrainTree、Authorize.net等)的体验如何?它必须:处理定期付款。有能力记入个人帐户。能够取消付款。有办法存储用户的付款详细信息(例如Authotize.netsCIM)。干杯 最佳答案 ActiveMerchant很棒,但在过去一年左右的时间里,我在使用它时发现了一些问题。首先,虽然某些网关可能会得到“支持”——但并非所有功能都包含在内。查看功能矩阵以确保完全支持您选择的网关-http