写在前面:本文乃标题党,不是月经贴,侧重于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 CodeUserDetailService(此处从数据库获取):
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 CodeAccessDecisionManager:
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所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则返回,权限不够则报错并调用权限不
参与评论
手机查看
返回顶部