SpringCloud微服务架构实战:商家权限体系设计及开发

商家管理后台与sso设计

在本文的电商平台实例中,商家是这个平台的主角,商家管理后台是专门为这个主角提供的一个安全可靠的操作平台。在商家管理后台中,商家可以进行商品管理、订单管理、物流管理、会员管理、评价管理等各个方面的管理工作。这些管理功能及其服务功能分别由不同的微服务项目实现,并通过不同的应用进行部署。现在我们要做的,就是将这些分布在不同应用中的管理功能,组成一个具有相同访问控制设计的管理后台。

单点登录(Single Sign On,SSO)设计可以将这种分散的应用,通过统一的访问控制和权限管理,整合成一个有机整体,为分布式环境中的不同应用,提供一个统一的登录控制和授权认证管理。商家管理员只需在任何一个应用中登录一次,就可以得到使用其他应用的权限。所以,不管商家管理后台的功能由多少个微服务应用组成,对于一个商家管理员来说,它始终只是一个完整的平台。

商家管理后台的设计和开发主要由商家管理开发和SSO开发两部分组成。其中,商家管理开发主要包含商家信息管理及其权限体系设计两部分。

这些设计集中在商家管理微服务项目merchant-microservice 中进行开发,完整的源代码可以从本书源代码中下载,本章的实例对应分支V2.1。

商家及其权限体系设计由 merchant-object、merchant-domain、merchant-restapi、merchant-client和 merchant-web等模块组成。SSO设计由merchant-sso模块和merchant-security模块组成。SSO的客户端接入可通过merchant-web模块进行体验。

SpringCloud微服务架构实战:商家权限体系设计及开发

商家权限体系的设计及开发

商家权限体系设计由权限管理模型和菜单管理模型两大功能模型组成。其中,权限管理模型包含商家、用户、角色等实体设计,菜单管理模型包含资源、模块、分类等实体设计。两大模型之间通过角色与资源的关联关系,组成一个完整的权限菜单体系结构,如图10-1所示。

SpringCloud微服务架构实战:商家权限体系设计及开发

在图10-1中,实体之间的关联关系使用单向关联设计,关联关系如下所示:

  • 用户从属于商家,是多对一的关联关系。
  • 用户拥有角色,是多对多的关联关系。
  • 角色拥有资源,是多对多的关联关系。
  • 资源从属于模块,是多对一的关联关系。
  • 模块从属于分类,是多对一的关联关系。

在图10-1所示的关联关系中,箭头所指一方为关联关系的主键,另一方为外键。其中,用户与角色、角色与资源分别使用一个中间表来存储关联关系。

这些对象所对应的物理模型,经过PowerDesigner 设计之后,最后完成的表格定义及其关联关系如图10-2所示。

SpringCloud微服务架构实战:商家权限体系设计及开发

在图10-2中,商家、用户、角色、资源、模块和分类等表格分别为t_merchant、t_usert_role.t_resource.t_model和 t_kind,用户与角色、角色与资源的关联关系的表格分别为user_role和role_resource。此外,表格persistent_logins是在用户处于登录状态时,用来存储临时数据的。

权限管理模型设计

权限管理模型主要由商家、用户、角色、资源、模块和分类等实体组成。下面对这些实体分别进行简要说明。

商家实体主要由ID、名称、邮箱、电话、地址、联系人和创建日期等属性组成,实现代码如下所示:

@Entity @Table(name = "t merchant") public class Merchant implements java.io.Serializable{ @Id @GeneratedValue(strategy =GenerationType.IDENTITY)private Long id; private String name;private String email;private String phone;private String address;private String linkman; @DateTimeFormat(pattern = "yyyY-MM-dd HH:mm:ss") @column (name = "created",columnDefinition = "timestamp defaultCurrent timestamp") @Temporal (TemporalType.TIMESTAMP)private Date created; // 0neToMany (cascade ={ },mappedBy ="merchant")// private List<User> users; public Merchant() { } ... }

用户实体主要由ID、名称、密码、邮箱、性别和创建日期等属性组成,实现代码如下所示:

@Entity @Table(name = "tuser") public class User implements java.io.Serializable{ @Id @GeneratedValue (strategy = GenerationType.IDENTITY)private Long id; private String name;private String password;private String email; eColumn (name = "sex",length= 1,columnDefinition = "tinyint")private Integer sex; @DateTimeFormat (pattern= "YyyY-MM-dd HH:mm: ss") eColumn (name = "created", columnDefinition = "timestamp defaultcurrent timestamp ") @Temporal (TemporalType.TIMESTAMP)private Date created; @ManyToMany (cascade = {,fetch = FetchType.EAGER)@JoinTable(name = "user role", joinColumns ={@JoinColumn (name = "user id")], inverseJoinColumns -{GJoinColumn (name = "role_id")}) private List<Role> roles; @ManyTo0ne CJoinColumn(name = "merchant_id")@JsonIgnore private Merchant merchant; public User() { } ... }

其中,@ManyToMany是一个多对多的正向关联关系,这里使用一个中间表user_role保存关联关系的数据。

@ManyToOne是一个反向关联设计,即使用mercnant_1a TFAh广oPIl Mo体建立关联关系。

角色实体由ID、名称和创建日期等属性组成,实现代码如下所示:

@Entity @Table(name ="t role") public class Role implements java.io.Serializable{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY)private Long id; private String name; @DateTimeFormat (pattern = "yyyy-MM-dd HH:mm:ss") eColumn(name = "created",columnDefinition = "timestamp defaultcurrent timestamp ") @Temporal (TemporalType.TIMESTAMP)private Date created; @ManyToMany(cascade = {}, fetch = FetchType.EAGER) @JoinTable(name = "role resource", joinColumns = {@JoinColumn (name - "role_id")}, inverseJoinColumns = {@JoinColumn (name = "resource_id"))) private List<Resource> resources; public Role() { } ... }

角色实体与资源实体是一个多对多的关联关系,因此使用@ManyToMany进行设置。通过这种关联关系,可以将权限管理模型与菜单管理模型组成一个完整的商家权限体系。

资源实体由ID、名称、统一资源定位和创建日期等属性组成,实现代码如下所示:

@Entity @Table(name ="t resource") public class Resource implements java.io.Serializable { @Id @GeneratedValue (strategy =GenerationType.IDENTITY)private Long id; private string name;private string url; @DateTimeFormat (pattern = "yyyy-MM-dd HH:mm:ss") @Column (name = "created", columnDefinition = "timestamp defaultcurrent timestamp") @Temporal(TemporalType.TIMESTAMP)private Date created; @ManyToOne @JoinColumn(name = "mid")@JsonManagedReference private Model model; public Resource() { } ... }

资源实体与模块的关联关系同样使用@ManyToOne进行反向关联设计,这与用户与商家的关联关系的设计原理相同。

模块实体由ID、名称、主机、图标和创建日期等属性组成,实现代码如下所示:

@Entity @Table(name ="tmodel") public class Model implements java.io.Serializable{ @Id @Generatedvalue(strategy -GenerationType.IDENTITY)private Long id; private String name;private String host;private String icon; @DateTimeFormat (pattern = "yyyy-MM-dd HH:mm:ss") eColumn (name = "created", columnDefinition = "timestamp defaultcurrent timestamp") @Temporal (TemporalType.TIMESTAMP)private Date created; @ManyTo0ne @JoinColumn (name = "kid")@JsonIgnore private Kind kind; public Model() {} ... }

模块实体的关联关系设计与资源实体的关联设计一样,也是使用@ManyToOne进行反向关联设计的。

分类实体由ID、名称、链接服务和创建日期等属性组成,实现代码如下所示:

@Entity @Table(name = "t kind") public class Kind implements java.io. Serializable{ @Id @Generatedvalue (strategy =GenerationType.IDENTITY)private Long id; private String name;private String link; @DateTimeFormat (pattern= "yyyy-MM-dd HH:mm:ss") cColumn (name = "created", columnDefinition = "timestamp defaultcurrent timestamp") @Temporal (TemporalType.TIMESTAMP)private Date created; public Kind() {} ... }

分类实体在菜单模型结构中是一个顶级菜单,所以不需要进行关联设计。

单向关联设计可以提高数据的访问性能,但也有不足的地方。比如,在角色实体中,已经实现了角色实体与资源实体的单向关联设计,因此从角色实体中查询资源列表,则是非常容易的。但是反过来,从资源实体中查询角色列表就有些费力了。为了弥补这种不足,可以使用SQIL查询语句实现,具体会在后面的持久化设计中进行说明。

权限管理模型的持久化设计

在权限管理模型设计完成之后,为各个实体创建一个存储库接口,并与JPA的存储库接口进行绑定,就可以给实体赋予操作行为,实现实体的持久化设计。这一过程,其实就是存储库接口设计的工作。

例如,可以创建一个如下所示的存储库接口实现商家实体的持久化设计:

@Repository public interface MerchantRepository extends JpaRepository<Merchant,Long>,JpaSpecificationExecutor<Merchant> { }

在这个接口设计中,通过继承JpaRepository,可以让这个接口具有增删改查的操作功能。再通过继承.JpaSpecificationExecutor,就可以进行复杂的分页查询设计。如果不做其他特殊的查询设计,这样就已经完成了商家实体的持久化设计了。

如果对于一个实体,还需要实现一些复杂的查询设计,如对用户实体进行持久化设计,则使用如下所示的代码:

@Repository public interface UserRepository extends JpaRepository<User, Long>,JpaSpecificationExecutor<User> { cQuery ("select distinct u from User u where u.name= :name")User findByName (@Param ("name") String name) ; @Query("select u from User u"+ " left join u.roles r""where r.name= :name") User findByRoleName (@Param ("name") String name) ; @Query ("select distinct u from User u where u.id= :id")User findById(@Param( "id") Long id); @Query ( "select u from User u"+ "left join u.roles r "+"where r.id = :id") List<User> findByRoleId(@Param("id") Long id); }

这里多了几个使用注解“@Query”进行自定义查询设计的声明方法。

其中,findByName和findByld主要使用distinct进行了去重查询,以避免在多对多的关联查询中,出现数据重复的情况。

另外,findByRoleName和 findByRoleld就是前面提到的,为弥补单向关联设计的不足而设计的查询。findByRoleName实现了从角色名称中查询用户列表的功能,而findByRoleld实现了从角色ID中查询用户列表的功能。

在角色实体存储库接口设计中,也需要增加一个查询设计,代码如下所示:

@Repository public interface RoleRepository extends JpaRepository<Role,Long>,JpaSpecificationExecutor<Role>{ CQuery("select o from Role o"+ "left join o.resources r"+"where r.id = :id") List<Role> findByResourceId(@Param("id") Long id); }

在这个设计中,findByResourceld是一个反向关联查询,即使用资源ID查询角色列表。

其他实体的持久化设计与商家实体的持久化设计类似,只需为它们创建一个存储库接口就可以了。

权限管理模型的服务封装

在领域服务开发中,服务层的实现是对存储库接口调用的一种封装设计,这样,不但可以在存储库接口调用过程中实现统一的事务管理,还可以增加其他功能。

下面我们以用户服务层的开发为例进行说明,其他各个业务服务层的开发与此类似,不再赘述。

在用户服务层的设计中,增删改查各个操作的实现代码如下所示:@Service

@Transactional public class UserService { CAutowired private UserRepository userRepository; public String insert (User user){ try { User old = findByName(user.getName());if(old == null) { userRepository.save(user); return user.getId() .toString();}else{ return "用户名' "+ old.getName()+ "'已经存在!"; }catch(Exception e){ e.printStackTrace();return e.getMessage(); public String update (User user){ try { userRepository.save(user); return user.getId() .toString();}catch(Exception e){ e.printStackTrace();return e.getMessage (); } } public String delete (Long id){ try { userRepository.deleteById(id);return id.toString (); }catch (Exception e){ e.printStackTrace(); return e.getMessage(); } } public User find0ne (Long id){ return userRepository.findByUserId(id); } public List<User> findA11(){ return userRepository.findAl1(); } }

在这个设计中,注解@Transactional 实现了隐式的事务管理功能。由于登录用户必须以用户名为依据,所以在新增用户名时,做了同名检测。

用户领域服务的分页查询功能的实现代码如下所示:

@service @Transactional public class UserService { CAutowired private UserRepository userRepository; public Page<User> findAl1 (UserQo userQo){ Sort sort = Sort.by(Sort.Direction. DESC, "created"); Pageable pageable = PageRequest.of (userQo.getPage(), userQo.getSize(. sort); return userRepository.findAll(new Specification<User>(){ @override public Predicate toPredicate(Root<User> root, CriteriaQuery<?>qu CriteriaBuilder criteriaBuilder) { List<Predicate> predicatesList =new ArrayList<Predicate>(); if (CommonUtils.isNotNull (userQo.getName())){ predicatesList.add (criteriaBuilder.like(root.get ("name"), "g"+userQo.getName() +"%"));  } if(CommonUtils.isNotNull (userQo.getMerchant ())){ predicatesList.add (criteriaBuilder.equal (root. get ("merchant"),userQo.getMerchant().getId())); } if (CommonUtils.isNotNull(userQo.getCreated())){ predicatesList.add(criteriaBuilder.greaterThan(root.get ("created"),userQo.getCreated())); query.where (predicatesList.toArray(new Predicate[predicatesList. size()])); return guery.getRestriction(); } }, pageable); } }

这里主要使用findAll 方法实现分页查询的功能,并通过查询对象userQo 传递查询参数,这些参数包含了用户名称、商家对象和创建日期等属性。

在领域服务设计中,我们使用了一些查询对象,这些查询对象统一在merchant-object模块中实现。查询对象的属性基本上与实体对象的属性相互对应,并且还增加了几个分页查询的属性。

查询对象的实现代码如下所示:

public class User0o extends PageQo implements java.io.Serializable{ private Long id; private String name; private String password;private String email;private Integer sex; @DateTimeFormat (pattern = "Yyyy-MM-dd HH:mm:ss")private Date created; private List<RoleQo>roles = new ArrayList<>(); private MerchantQo merchant; public UserQ0() { } ... }

在完成服务层开发之后,商家权限体系的设计基本告一段落。

您可能还会对下面的文章感兴趣: