Erlo

一文助您成为Java.Net双平台高手

2019-02-15 14:03:57 发布   543 浏览  
页面报错/反馈
收藏 点赞

写在前面:本文乃标题党,不是月经贴,侧重于Web开发差异,或细节或概述,若有不对之处,还请各位读者本着友好互助的心态批评指正。由于博客园中.Neter较多(个人感觉),因此本文也可以作为.Neter到Java开发的快速入门。


总述

在.Net开发中,微软官方框架类可以很好的解决的大部分问题,开发人员可以心安理得的在一亩三分地腾挪躲闪出花来;偶有一些优(zhao)秀(chao)的开源库,各库的关注点也基本不会重样;所以.Neter只要按部就班即可。而Java喜欢定义各种规范,各路大神各自实现,因此一个概念常常会有很多的第三方库,虽然有Spring这种杀手级框架,不过基于IOC和AOP的设定,Spring家族也变得异常庞大,在编码时需要引入大量的annotation来织入逻辑;虽然貌似最大程度的解耦了各组件,但导致代码的可读性和可调试性非常不好,碎片化非常严重。不过也因为如此,Java社区成为设计思想的孕育地,并常常出现一些让人击节的设计模式。其中的概念传播到隔壁.Net圈,圈内小白往往一脸懵逼,而少数大佬不管不顾拿来套用,往往是用错了,或者让人不知所以。

笼统来说,.Net框架隐藏细节,简便清晰,套路单一,但常陷入知其然不知其所以然的懵逼境地;Java&Spring注解隐藏细节,概念繁多,没有方向感或有被绕晕的风险,但一旦破位而出,则纵横捭阖天地之大可任意施展至其它平台。不过两者差异随着.Net的开源以肉眼不可见的速度缓慢消失,特别是最近几年,.Net在语法层面已经超越了Java良多,Java虽然一时半会抹不开面子,但也一直在改进。到的本文撰写时分,借用不知名网友语:“C#语法已经达到Java20,用户量撑死Java7,生态Java1.4”。

两者竞争主要集中在Web开发领域。目前在该领域,Spring Boot已基本成为事实上Java平台的“官方框架”,我想大部分开发人员并不会在意背后的实现细节,从这个方面来讲,两个平台的开发模式有一定程度的相似。


数据持久层

为啥这节标题不是ORM呢?毕竟ORM现在是业界标准,很难想象这个时代还需要手写SQL,还需要手动操作JDBC/ADO;如果你打算这么干,一定会被年轻一辈打心眼里鄙视:)

Java

ORM:十多年前,Hibernate就开始兴起,它提供了半对象化的HQL和完全的面向对象QBC。之后也出现了其它一些ORM比如TopLink。

JPA:JDK5引入,是SUN公司为了统一目前众多ORM而提出的ORM规范(又犯了定义规范的瘾)。这个规范出来后,很多ORM表示支持,但以前的还得维护啊,所以像Hibernate就另外建了一个分支叫Hibernate JPA。网友benjaminlee1所言:“JPA的出现只是用于规范现有的ORM技术,它不能取代现有的Hibernate等ORM框架,相反,采用JPA开发时,我们仍将使用这些ORM框架,只是此时开发出来的应用不在依赖于某个持久化提供商。应用可以在不修改代码的情况下载任何JPA环境下运行,真正做到低耦合,可扩展的程序设计。类似于JDBC,在JDBC出现以前,我们的程序针对特性的数据库API进行编程,但是现在我们只需要针对JDBC API编程,这样能够在不改变代码的情况下就能换成其他的数据库。”

Spring Data JPA:有了JPA,我们就可以不在意使用哪个ORM了,但是Spring Data JPA更进一步(为Spring家族添砖加瓦),按约定的方式自动给我们生成持久化代码,当然它底层还是要依赖各路ORM的。相关资料:使用 Spring Data JPA 简化 JPA 开发

Mybatis:随着时间的流逝,Hibernate曾经带来的荣耀已经被臃肿丑陋的配置文件,无法优化的查询语句淹没。很多人开始怀念可一手掌控数据操作的时代,于是Mybatis出现了。Mybatis不是一个完整的ORM,它只完成了数据库返回结果到对象的映射,而存取逻辑仍为SQL,写在Mapper文件中,它提供的语法在一定程度上简化了SQL的编写,最后Mybatis将SQL逻辑映射到接口方法上(在Mapper文件中指定<mapper namespace="xxx">,其中xxx为映射的DAO接口)。针对每个表写通用增删改查的Mapper SQL既枯燥又易出错,所以出现了Mybatis-Generator之类的代码生成工具,它能基于数据表生成实体类、基本CRUD的Mapper文件、对应的DAOInterface。

Mybatis-Plus:在Mybatis的基础上,提供了诸如分页、复杂条件查询等功能,基础CRUD操作不需要额外写SQL Mapper了,只要DAO接口继承BaseMapper接口即可。当然为了方便,它也提供了自己的代码生成器。

.NET

  ORM:主流Entity Framework,除开ORM功能外,它还提供了Code first、DB first、T4代码生成等特性。性能上与Hibernate一个等级,但使用便捷性和功能全面性较好,更别说还有linq的加持。


认证&授权&鉴权

认证是检测用户/请求是否合法,授权是赋予合法用户相应权限,鉴权是鉴别用户是否有请求某项资源的权限(认证和授权一般是同时完成)。我们以web为例。

C#/Asp.net mvc

提供了两个Filter:IAuthenticationFilter 和 AuthorizeAttribute,前者用于认证授权,后者用于鉴权。

 1 //IAuthenticationFilter 认证,认证是否合法用户
 2 public class AdminAuthenticationFilter : ActionFilterAttribute, IAuthenticationFilter
 3 {
 4     public void OnAuthentication(AuthenticationContext filterContext)
 5     {
 6         IPrincipal user = filterContext.Principal;
 7         if (user == null || !user.Identity.IsAuthenticated)
 8         {
 9             HttpCookie authCookie = filterContext.HttpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
10             if (authCookie != null)
11             {
12                 FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
13                 if (ticket != null && !string.IsNullOrEmpty(ticket.UserData))
14                 {
15                     var userId = Convert.ToInt32(ticket.UserData);
16                     user = EngineContext.Resolve<PFManagerService>().GetManager(userId);
17                     filterContext.Principal = user; //后续会传递给HttpContext.Current.User
18                 }
19             }
20         }
21     }
22 
23     public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
24     {
25         // 认证失败执行
26     }
27 }
View Code

认证成功后,将user赋给filterContext.Principal(第17行),filterContext.Principal接收一个IPrincipal接口对象,该接口有个 bool IsInRole(string role) 方法,用于后续的鉴权过程。

 1 public class AdminAuthorizationFilter : AuthorizeAttribute
 2 {        
 3     public override void OnAuthorization(AuthorizationContext filterContext)
 4     {
 5         //childaction不用授权
 6         if (filterContext.IsChildAction)
 7             return;
 8 
 9         if (!filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true) && !filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true))
10         {           
11             if (filterContext.HttpContext.User != null && filterContext.HttpContext.User.Identity.IsAuthenticated)
12             {
13                 var controllerName = filterContext.RouteData.Values["controller"].ToString().ToLower();
14                 var actionName = filterContext.RouteData.Values["action"].ToString().ToLower();
15                 //只要登录,则都能访问工作台
16                 if (controllerName.ToLower() == "home" && actionName.ToLower() == "index")
17                     this.Roles = string.Empty;
18                 else
19                 {
20                     var roleIds = EngineContext.Resolve<BEModuleService>().GetRoleIdsHasModuleAuthorization(controllerName, actionName, MasonPlatformType.AdminPlatform);
21                     if (roleIds == null)
22                     {
23                         filterContext.Result = new HttpNotFoundResult();
24                         return;
25                     }
26                     //将资源所需权限赋给成员变量Roles
27                     this.Roles = string.Join(",", roleIds);
28                 }
29             }
30         }
31 
32         base.OnAuthorization(filterContext);
33     }
34 }
View Code

注意第27行,我们将拥有该资源的所有权限赋给Roles,之后AuthorizeAttribute会循环Roles,依次调用当前用户(上述的filterContext.Principal)的IsInRole方法,若其中一个返回true则表明用户有访问当前资源的权限。

Java/Spring Security

也提供了两个类,一个Filter和一个Interceptor:AuthenticationProcessingFilter用于用户认证授权,AbstractSecurityInterceptor用于鉴权。Spring Security基于它们又封装了几个类,主要几个:WebSecurityConfigurerAdapter、FilterInvocationSecurityMetadataSource、AccessDecisionManager、UserDetailsService。另外还有各类注解如@EnableGlobalMethodSecurity等。(以下代码含有一点jwt逻辑)

WebSecurityConfigurerAdapter:

 1 @Configuration
 2 @EnableWebSecurity
 3 @EnableGlobalMethodSecurity(prePostEnabled = true)
 4 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 5     @Autowired
 6     private JwtAuthenticationEntryPoint unauthorizedHandler;
 7 
 8     @Autowired
 9     private UserDetailsService userDetailsService;
10 
11     @Autowired
12     private CustomPostProcessor postProcessor;
13 
14     @Autowired
15     public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
16         authenticationManagerBuilder
17                 .userDetailsService(this.userDetailsService)
18                 .passwordEncoder(passwordEncoder());
19     }
20 
21     @Bean
22     public PasswordEncoder passwordEncoder() {
23         return new BCryptPasswordEncoder();
24     }
25 
26     @Bean
27     public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
28         return new JwtAuthenticationTokenFilter();
29     }
30 
31     @Override
32     protected void configure(HttpSecurity httpSecurity) throws Exception {
33         httpSecurity
34                 // we don't need CSRF because our token is invulnerable
35                 .csrf().disable()
36                 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
37                 // don't create session
38                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
39                 .authorizeRequests()
40                 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
41                 .anyRequest().authenticated().withObjectPostProcessor(postProcessor);
42 
43         // Custom JWT based security filter
44         httpSecurity
45                 .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
46     }
47 }
View Code

主要关注两个方法configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder)和configure(HttpSecurity httpSecurity)。configureAuthentication主要用于设置UserDetailsService,加载用户数据需要用到;configure用于设置资源的安全级别以及全局安全策略等。第41行withObjectPostProcessor,用于设置FilterInvocationSecurityMetadataSource和AccessDecisionManager,它们两个用于鉴权,下面会讲到。

 1 @Component
 2 public class CustomPostProcessor implements ObjectPostProcessor<FilterSecurityInterceptor> {
 3     @Autowired
 4     private CustomFilterSecurityMetadataSource customFilterSecurityMetadataSource;
 5 
 6     @Autowired
 7     private CustomAccessDecisionManager customAccessDecisionManager;
 8 
 9     @Override
10     public <T extends FilterSecurityInterceptor> T postProcess(T fsi) {
11         fsi.setSecurityMetadataSource(customFilterSecurityMetadataSource); //1.路径(资源)拦截处理
12         fsi.setAccessDecisionManager(customAccessDecisionManager); //2.权限决策处理类
13         return fsi;
14     }
15 }
View Code

UserDetailService(此处从数据库获取):

 1 @Service
 2 public class JwtUserDetailsServiceImpl implements UserDetailsService {
 3 
 4     @Autowired
 5     private UserRepository userRepository;
 6 
 7     @Override
 8     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 9         User user = userRepository.findByUsername(username);
10 
11         if (user == null) {
12             throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
13         } else {
14             return JwtUserFactory.create(user);
15         }
16     }
17 }
View Code

注意loadUserByUsername需要的参数名username是约定好的,在UsernamePasswordAuthenticationFilter中定义,value是从HttpServletRequest中获取。

FilterInvocationSecurityMetadataSource(用于获取当前请求资源所需的权限):

 1 /**
 2  * 路径拦截处理类
 3  * <p>
 4  * 如果路径属于允许访问列表,则不做拦截,放开访问;
 5  * <p>
 6  * 否则,获得路径访问所需角色,并返回;如果没有找到该路径所需角色,则拒绝访问。
 7  */
 8 @Component
 9 public class CustomFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
10     @Autowired
11     private ApiRepository apiRepository;
12 
13     @Override
14     public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
15         FilterInvocation fi = (FilterInvocation) object; //当前请求对象
16 
17         List<ConfigAttribute> configAttributes = getMatcherConfigAttribute(fi.getRequestUrl(), fi.getRequest().getMethod()); // 获得访问当前路径所需要的角色
18 
19         return configAttributes.size() > 0 ? configAttributes : deniedRequest(); //返回当前路径所需角色,如果路径没有对应角色,则拒绝访问
20     }
21 
22     @Override
23     public Collection<ConfigAttribute> getAllConfigAttributes() {
24         return null;
25     }
26 
27     @Override
28     public boolean supports(Class<?> clazz) {
29         return FilterInvocation.class.isAssignableFrom(clazz);
30     }
31 
32     /**
33      * 获取当前路径以及请求方式获得所需要的角色
34      *
35      * @param url 当前路径
36      * @return 所需角色集合
37      */
38     private List<ConfigAttribute> getMatcherConfigAttribute(String url, String method) {
39         Set<Authority> authorities = new HashSet<>();
40         // 1.根据url的开头去数据库模糊查询相应的api
41 
42         String prefix = url.substring(0, url.lastIndexOf("/"));
43 
44         prefix = StringUtil.isEmpty(prefix) ? url : prefix + "%";
45 
46         List<Api> apis = apiRepository.findByUriLikeAndMethod(prefix, method);
47 
48         // 2.查找完全匹配的api,如果没有,比对pathMatcher是否有匹配的结果
49         apis.forEach(api -> {
50             String pattern = api.getUri();
51 
52             if (new AntPathMatcher().match(pattern, url)) {
53                 List<Resource> resources = api.getResources();
54 
55                 resources.forEach(resource -> {
56                     authorities.addAll(resource.getAuthorities());
57                 });
58             }
59         });
60 
61         return authorities.stream().map(authority -> new SecurityConfig(authority.getId().toString())).collect(Collectors.toList());
62     }
63 
64     /**
65      * @return 默认拒绝访问配置
66      */
67     private List<ConfigAttribute> deniedRequest() {
68         return Collections.singletonList(new SecurityConfig("ROLE_DENIED"));
69     }
70 }
View Code

AccessDecisionManager:

 1 /**
 2  * 权限决策处理类
 3  *
 4  * 判断用户的角色,如果为空,则拒绝访问;
 5  *
 6  * 判断用户所有的角色中是否有一个包含在 访问路径允许的角色集合中;
 7  *
 8  * 如果有,则放开;否则拒绝访问;
 9  */
10 @Component
11 public class CustomAccessDecisionManager implements AccessDecisionManager {
12     @Override
13     public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
14         if (authentication == null) {
15             throw new AccessDeniedException("permission denied");
16         }
17 
18         //当前用户拥有的角色集合
19         List<String> roleCodes = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
20 
21         //访问路径所需要的角色集合
22         List<String> configRoleCodes = configAttributes.stream().map(ConfigAttribute::getAttribute).collect(Collectors.toList());
23         for (String roleCode : roleCodes) {
24             if (configRoleCodes.contains(roleCode)) {
25                 return;
26             }
27         }
28 
29         throw new AccessDeniedException("permission denied");
30     }
31 
32     @Override
33     public boolean supports(ConfigAttribute attribute) {
34         return true;
35     }
36 
37     @Override
38     public boolean supports(Class<?> clazz) {
39         return true;
40     }
41 }
View Code

上述第19行和第22行分别为UserDetailService处取到的用户拥有的权限和FilterInvocationSecurityMetadataSource取到的访问资源需要的权限,两者对比后即得出用户是否有访问该资源的权限。具体来说,鉴权的整个流程是:访问资源时,会通过AbstractSecurityInterceptor拦截器拦截,其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则返回,权限不够则报错并调用权限不

登录查看全部

参与评论

评论留言

还没有评论留言,赶紧来抢楼吧~~

手机查看

返回顶部

给这篇文章打个标签吧~

棒极了 糟糕透顶 好文章 PHP JAVA JS 小程序 Python SEO MySql 确认