网络日志

malagu认证与授权之@malagu/security

导言

  • 本文已参与「开源摘星计划」,欢迎正在阅读的你加入。活动链接:https://github.com/weopenproj...
  • malagu的认证与授权参考spring-security思想,详细介绍请移步官方文档。malagu除了基本的security外提供了ODIC 的认证和 OAuth2.0 的授权能力,本文主要介绍@malagu/security组件的基本应用实践。

认证与授权组件 @malagu/security的简单应用

  • 1.添加组件在项目中引用@malagu/security组件

    yarn add @malagu/security # 或者 npm i @malagu/security
  • 2.重写loginUrl和logoutUrl定义登录和注销接口和请求方式

    malagu:
    security:
      loginUrl: /api/login
      loginMethod: POST    
      logoutUrl: /api/logout
  • 3.重写UserService实现自定义登录注册用户时密码需要使用PasswordEncoder生成;我们只需将用户和密码在load()中赋值给security组件User即可,校验比对逻辑交由@malagu/security组件完成。

    import { Component, Autowired } from '@malagu/core';
    import { UserService, UsernameNotFoundError, AccountStatusError, PasswordEncoder } from '@malagu/security/lib/node';
    import { User, ElPolicy, PolicyType, AuthorizeType } from '@malagu/security';
    import { OrmContext, Transactional } from "@malagu/typeorm/lib/node";
    import { UserEntity } from '@microservice/datasource';
    
    /**
     * 重写UserService实现自定义登录
     * @param username 登录名可以是用户名称(user_name)或者电话(mobile), 优先级:user_name > mobile
     */
    @Component({ id: UserService, rebind: true })
    export class UserServiceImpl implements UserService<string, User> {
    
      @Autowired(PasswordEncoder)
      protected readonly passwordEncoder: PasswordEncoder;
    
      @Transactional({ readOnly: true })
      async load(username:string): Promise<User>{
          const repo = OrmContext.getRepository(UserEntity);
    
          let user = await repo.findOne({ userName: username })       
          if (!user) {
            user = await repo.findOne({ mobile: username })
          }
      
          if (!user) {
            throw new UsernameNotFoundError();
          }
          if(user.state == false){
            throw new AccountStatusError();
          }
          
          return  {
              type: "",
              username: user.userName,
              password: user.password,
              policies: [ <ElPolicy>{
                  type: PolicyType.el,
                  authorizeType: AuthorizeType.Pre,
                  el: 'true'
                } ],
              accountNonExpired: true,
              accountNonLocked: true,
              credentialsNonExpired: true,
              enabled: true
          }
      }
    
    }
  • 4.重写认证失败处理器AuthenticationErrorHandler

    import { Component, Autowired } from '@malagu/core';
    import { HttpStatus } from '@malagu/web';
    import { ErrorHandler, Context, RedirectStrategy } from '@malagu/web/lib/node';
    import { AuthenticationErrorHandler, 
      AUTHENTICATION_ERROR_HANDLER_PRIORITY, 
      AuthenticationError  } from '@malagu/security/lib/node'
    
    @Component({ id: AuthenticationErrorHandler, rebind: true })
    export class AuthenticationErrorHandlerImpl implements ErrorHandler{
      readonly priority: number = AUTHENTICATION_ERROR_HANDLER_PRIORITY;
    
      @Autowired(RedirectStrategy)
      protected readonly redirectStrategy: RedirectStrategy;
    
    
      canHandle(ctx: Context, err: Error): Promise<boolean> {
          return Promise.resolve(err instanceof AuthenticationError);
      }
    
      async handle(ctx: Context, err: AuthenticationError): Promise<void> {
          let message = "";
          switch (err.name) {
              case "UsernameNotFoundError":
                  ctx.response.statusCode = HttpStatus.FORBIDDEN;    
                  message = "用户不存在";           
                  break; 
              case "BadCredentialsError":
                  ctx.response.statusCode = HttpStatus.FORBIDDEN;    
                  message = "用户密码错误";           
                  break;  
              case "AccountStatusError":
                  ctx.response.statusCode = HttpStatus.FORBIDDEN;    
                  message = "用户被冻结";           
                  break;
              case "AuthenticationError":
                  ctx.response.statusCode = HttpStatus.UNAUTHORIZED;    
                  message = "用户没有访问权限,需要进行身份认证";           
                  break;            
              default:
                  ctx.response.statusCode = HttpStatus.UNAUTHORIZED;
                  message = err.message;
                  break;
          }        
          ctx.response.end(message);
      }
    
    }
    
  • 5.重写认证成功处理器AuthenticationSuccessHandler非必需,不重写将跳转到首页

    import { Component } from '@malagu/core';
    import { HttpStatus } from '@malagu/web';
    import { AuthenticationSuccessHandler, Authentication } from '@malagu/security/lib/node'
    import { Context } from '@malagu/web/lib/node'
    
    @Component({ id: AuthenticationSuccessHandler, rebind: true })
    export class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
    
      async onAuthenticationSuccess(authentication: Authentication): Promise<void> {
          Context.getResponse().statusCode = HttpStatus.OK;
          Context.getResponse().body = JSON.stringify({ username: authentication.name });
      }
    
    }
  • 6.重新登出处理器LogoutSuccessHandler非必需,不重写将跳转到登录页

    import { LogoutSuccessHandler, LOGOUT_SUCCESS_HANDLER_PRIORITY } from '@malagu/security/lib/node';
    import { Component } from '@malagu/core';
    import { HttpStatus } from '@malagu/web';
    import { Context } from '@malagu/web/lib/node';
    
    @Component({id: LogoutSuccessHandler, rebind: true })
    export class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
    
      readonly priority = LOGOUT_SUCCESS_HANDLER_PRIORITY;
    
      async onLogoutSuccess(): Promise<void> {
          Context.getResponse().statusCode = HttpStatus.OK;
          Context.getResponse().body = "登出成功";
      }
    }
    
  • 7.@Authenticated的使用可以在controller类上使用,这样该类下的所有开放接口都需要鉴权

    @Controller("user")
    @Authenticated()
    export class UserController {
    
      @Autowired(UserInfoService)
      protected userInfoService: UserInfoService;
      ... ...
    }

    也可以在指定的接口上使用

      @Get("/:userId")
      @Json()
      @Authenticated()
      async getUserInfo(@Param("userId") userId: number){
          const result = await this.userInfoService.getUserInfo(userId);     
          return result  
    
      }
  • 8.除了@Authenticated,malagu还提供了用于权限控制的装饰器@PreAuthorize以及匿名@Anonymous

结语

至此,@malagu/security的核心代码就已经完成。在module.ts文件引用,运行项目我们就可以进行的调试了。由于登录逻辑都交由组件处理了,malagu的认证授权还是比较简单的。

思考

  • @malagu/security的原理是怎样的?
  • UserService中返回体User各属性的含义?
  • 登录有效期怎么设置?
  • 可以使用@malagu/security实现单点登录吗?

本文为学习类文章,如有错误欢迎指正!思考内容欢迎各位大佬答疑解惑。