实现了数据库逻辑封装和连接Mysql数据库后,我们就可以实现以下后台所需要的相关接口了。
首先声明,我们在Application中,注册的Json解析器原本是Jackson,现在替换成GsonProvider,因为Jackson在解析boolean数据的时候会产生性能问题。
在我们以后返回的实体类中,每个属性都要加@Expose注解,否则GsonProvider将不进行解析。最直白的表现就是Post请求返回为空。
因为接口访问,难免要传入各种数据,返回各种数据,我们这里在bean包下新建一个card包,因为有些敏感信息是在不适合直接返回,比如密码这种,所以还是要新建实体类。以后用于接口请求返回的实体类信息都存放在这个包中。
所有的接口都放在service包下。
注册接口实现:
注册接口,首先客户端需要传入一些相关信息:账号,密码,用户名等。
然后我们需要返回用户账号的相关信息。入参,和返回,也就对应了两个实体类:
入参:RegisterModel,返回:UserCard。
两者实现也很简单:
public class RegisterModel {
@Expose //必须加此注解,否则Gson不进行解析,配置是在GsonProvider中的
private String account;
@Expose
private String password;
@Expose
private String name;
//..getter 、setter
}
public class UserCard {
@Expose
private String id;
@Expose
private String name;
@Expose
private String portrait;
@Expose
private String desc;
@Expose
private int sex=0;
//用户信息最后的更新时间
@Expose
private LocalDateTime modifyAt=LocalDateTime.now();
//用户粉丝的数量
@Expose
private int following;
//我与当前User的关系状态,是否已经关注了这个人
@Expose
private int isFollow;
//...getter 和setter
}
可以看出这两个类的实现也很简单,为了节省篇幅,我们没有将getter和setter贴出来,自己别忘了实现就行。
唯一需要注意的点,就是我们所以的属性字段都加了@Expose注解,前面也说了如果属性字段不加这个注解,那根据我们GsonProvider里的设置,是不会解析对应字段的。
有了这个两个实体类,我们就可以在AccountService中实现我们的注册接口了:
/**
* 注册
* @param model
* @return
*/
@POST
@Path("/register")
@Consumes(MediaType.APPLICATION_JSON)//传入json
@Produces(MediaType.APPLICATION_JSON)//输出json
public UserCard register(RegisterModel model){//注册需要传入一些信息,然后返回User,但是
UserCard card=new UserCard();
card.setName(model.getName());
card.setIsFollow(true);
return card;
}
重启一下服务器,测一下接口是否可以访问:
成功返回数据。
接下来就是进行我们业务操作的时候了:
每个请求接口后面真正的后台逻辑实现,首先我们新建一个factory包,里面存储我们业务层的代码。
因为首先编写的是注册接口,有关用户信息。所以我们新建一个UserFactory的类,实现一下register的业务逻辑:
/**
* 用户注册等后台逻辑操作
*/
public class UserFactory {
/**
* 用户注册---注册的操作需要写入数据库,并返回数据库中的User信息
* @param account
* @param password
* @param name
* @return
*/
public static User register(String account,String password,String name){
User user=new User();
user.setName(name);
user.setPassword(password);
user.setPhone(account);//采用手机号注册
//数据库操作
//首先穿件一个会话
Session session= Hib.session();
//开启一个事务
session.beginTransaction();
try{
//保存
session.save(user);
//提交事务
session.getTransaction().commit();
}catch (Exception e){
session.getTransaction().rollback();//回滚事务
return null;
}
return user;
}
}
然后在我们直接接口所在的AccountService中调用:
/**
* 注册
* @param model
* @return
*/
@POST
@Path("/register")
@Consumes(MediaType.APPLICATION_JSON)//传入json
@Produces(MediaType.APPLICATION_JSON)//输出json
public UserCard register(RegisterModel model){//注册需要传入一些信息,然后返回User,但是
User user= UserFactory.register(model.getAccount()
,model.getPassword()
,model.getName());
if (user!=null){
UserCard card=new UserCard();
card.setName(model.getName());
card.setPhone(user.getPhone());
card.setSex(user.getSex());
card.setIsFollow(true);
card.setModifyAt(user.getUpdateAt());
return card;
}
return null;
}
然后重启服务器,调用一下我们的接口:
这次请求时间会长一点,因为Hibernate需要给我们自动建表。
图太长了。就贴这两个,Hibernate会帮我们将之前创建的实体类对应的数据库表都建立出来,使用Navicat看一下我们的数据库:
再看一下我们的请求结果:
还是有问题需要处理的,最大的问题就是密码明文。如果密码是明文的情况下,如果我们数据库被攻击了,那么用户账号密码泄露,就会造成很严重的很严重的问题。
针对这个问题,我们需要进行一下加密:
这里的MD5编码和Base64加密用的都是网上最简单的方法,就不累述了。
除了密码问题之外,要知道我们在设计User表的时候,设计过name和phone唯一,所以被注册过的用户名和手机号,肯定不能二次注册:
因此在注册的时候,我们要先去数据库查一下要注册的用户名和名称是否已经被注册过的。
因为每次都写一遍session.beginTransaction很不友好,代码也冗余,所以我们封装一个方法,只需要将具体逻辑抛给API层写就行了:在Hib类中添加如下方法和接口:
/**
* 将具体实现交给上层
*/
public interface Query<T>{
T query(Session session);
}
public interface QueryOnly{
void query(Session session);
}
/**
* 只查询,不返回结果
* @param query
*/
public static void queryOnly(QueryOnly query){
//重开一个Session
Session session =sessionFactory.openSession();
final Transaction transaction=session.beginTransaction();
try{
query.query(session);
}catch (Exception e){
e.printStackTrace();
//出错回滚事务
transaction.rollback();
}finally {
//无论成功失败都要关闭
session.close();
}
}
/**
* 传递一个接口,并将query传递进去
* 泛型方法,可能需要返回值
* @param query
*/
public static <T> T query(Query<T> query){
//重开一个Session
Session session =sessionFactory.openSession();
final Transaction transaction=session.beginTransaction();
T t=null;
try{
t= query.query(session);
}catch (Exception e){
e.printStackTrace();
//出错回滚事务
transaction.rollback();
}finally {
//无论成功失败都要关闭
session.close();
}
return t;
}
这里有个致命错误!!!!就是我们的事务没有提交!!!
在t=query.query(session)下一定不要忘了提交事务!!
为了提醒自己,我决定不在上边添加,以此警示!
// 提交
transaction.commit();
在UserFactory中写一下查询逻辑:
/**
* 查找手机号是否已经注册
* @param phone
* @return
*/
public static User findByPhone(String phone){
return Hib.query(session -> (User)session.createQuery("from User where phone=:inPhone")
.setParameter("inPhone",phone)
.uniqueResult());
}
/**
* 查找Name是否已经被注册-----昵称相同有错吗...
* @param name
* @return
*/
public static User findByName(String name){
return Hib.query(session -> (User)session.createQuery("from User where name=:name")
.setParameter("name",name)
.uniqueResult());
}
然后在AccountService中调用即可:
//手机号是否已经被注册
User user=UserFactory.findByPhone(model.getAccount().trim());
if (user!=null){
UserCard card=new UserCard();
card.setName("该手机号已经被注册过了!");
return card;
}
//名字是否已经被注册
user=UserFactory.findByName(model.getName().trim());
if (user!=null){
UserCard card=new UserCard();
card.setName("该用户名已经被注册过了!");
return card;
}
通过UserCard虽然能返回用户信息,出错返回null。但这个null代表的是用户名已经被注册了,还是手机号被注册了,这点如果通过上边这种方式,直接写死,一方面扩展性不强,另一方面如果后续我们要返回群组信息,也通过这种方式,规范性不强。服务端出错后,我们的返回信息最好要有统一的规范,方便管理,也方便后续的业务扩展。所以,封装一个基础的ResponseModel
用来处理返回信息,以及出错出错的异常信息,这一步十分有必要。
接下来,一方面进行封装,一方面我们来实现以下登陆接口,具体流程如下:
1.基础返回Model封装,统一整个接口的API规范
2.账户登陆注册部分的返回信息封装
3.账户登陆逻辑的实现,Token的生效机制
4.绑定推送设备id的接口同步实现
本章新增了一个类:ResponseModel,就是利用泛型,返回各种类型信息的报错详细。类内容有点长,这里就不贴了。具体可参考仓库,没有难点。
接着我们开始说注册登陆这两个接口,注册接口我们虽然实现了,返回的UserCard信息。但是一些关键性信息,比如Token、account(确实需要单独抽离出来,以防后续不适用手机号来作为账号),是否绑定设备等信息这些我们是没有通过UserCard传递的。(为什么不将Token存放在Card中返回给客户端呢?这地方是个思想,得好好研究一下!)
当用户成功注册登陆后,上面这些UserCard中没有的信息,我们也需要返回给客户端,所以就得再加一个包装类:AccountRspModel。具体实现很简单:
public class AccountRspModel {
//用户基本信息
@Expose
private UserCard user;
//当前绑定的账号
@Expose
private String account;
//当前用户的token
@Expose
private String token;
//是否已经被绑定到设备
@Expose
private boolean isBind;
public AccountRspModel(User user){
this(user,false);
}
public AccountRspModel(User user,boolean isBind){
this.user=new UserCard(user);//UserCard新增一个构造方法,通过user赋值
this.account=user.getPhone();
this.token=user.getToken();
this.isBind=isBind;
}
//..getter &setter
}
@Expose不要忘了加。
然后修改一下我们的register方法:
@POST
@Path("/register")
@Consumes(MediaType.APPLICATION_JSON)//传入json
@Produces(MediaType.APPLICATION_JSON)//输出json
public ResponseModel<AccountRspModel> register(RegisterModel model) {//注册需要传入一些信息,然后返回User,但是
//手机号是否已经被注册
User user = UserFactory.findByPhone(model.getAccount().trim());
if (user != null) {
return ResponseModel.buildHaveAccountError();
}
//名字是否已经被注册
user = UserFactory.findByName(model.getName().trim());
if (user != null) {
return ResponseModel.buildHaveNameError();
}
//开始注册逻辑
user = UserFactory.register(model.getAccount()
, model.getPassword()
, model.getName());
if (user != null) {//已经注册成功
AccountRspModel accountModel=new AccountRspModel(user);
return ResponseModel.buildOk(accountModel);
}else{//异常错误
return ResponseModel.buildRegisterError();
}
}
这样返回的信息就是ResponeModel处理过的信息了,正常返回信息和错误都在ResponseModel里进行了相关处理。
重启服务器,看一下结果:
分别是注册成功和注册失败的返回。
用户在注册成功后,我们是要实现对应的登陆功能的,毕竟注册功能后,也不能给用户账号密码让他重新登陆一遍,体验不好。
因为我们的注册登陆的业务实现都是在UserFactory里实现的,所以要增加注册完登陆的功能肯定要从这个类的register下手,此外每次登陆都要生成新的Token这是必须的,所以我们把login进行了抽取,这个login方法可以用来我们register和login两个接口里的登陆操作,省的再写一遍login方法里的逻辑:
public static User register(String account,String password,String name){
//处理密码
password=encodePassword(password);
User user=createUser(account,password,name);
if (user!=null){//注册成功,进行登录操作
user=login(user);
}
return user;
}
private static User login(User user){
//主要就是更新Token操作,使用一个随机的UUID值充当Token
String newToken= UUID.randomUUID().toString();
//进行一次Base64格式化
newToken=TextUtil.encodeBase64(newToken);
user.setToken(newToken);
//Token也要更新到数据库中
return Hib.query(session ->{
session.saveOrUpdate(user);
return user;
} );
}
有一个点,被我们忽略了,就是用户登录或者注册的时候,如果传了一些非法的json字符串,里面没有我们想要的属性,也会报空指针异常。所以,在注册登陆的时候,需要对参数进行一下判断。也就是在LoginModel和RegisterModel里加一下判断方法:
public static boolean check(RegisterModel registerModel){
return registerModel!=null
&&!Strings.isNullOrEmpty(registerModel.account)
&&!Strings.isNullOrEmpty(registerModel.password)
&&!Strings.isNullOrEmpty(registerModel.name);
}
同理LoginModel,只不过login的时候不需要判断name,因为也用不着。
接着在我们的接口中判断一下即可:
if (!RegisterModel.check(model)) {//对输入参数进行校验
return ResponseModel.buildParameterError();
}
关于用户信息这块,还有一个至关重要的点,就是我们的PushId,因为我们的消息推送,比如一条消息要推给A,那么通过什么来判断A呢?答案就是pushId,这个pushId必须慎重处理,不能混乱。毕竟一条本该推给A的消息,你推给了B,A是你老婆,你说,亲爱的,今晚我们xx宾馆见。结果推给了B,B是你兄弟,这样搞,你不怕你兄弟给你讲哲学吗…
这个pushId由客户端传过来,为什么由客户端传递呢?因为每个设备都有唯一的设备ID,你拿到这个唯一的设备ID,也就锁定了某一个人。那么接下来,我们就加个bindPushId方法,用户将pushId存储到对应的User上,还是在UserFactory里添加方法:
public static User bindPushId(User user,String pushId){
if (Strings.isNullOrEmpty(pushId)){
return null;
}
//第一步,查询是否有其他账户绑定了这个设备
//取消绑定,避免推送混乱
//查询的列表不能包括自己
Hib.queryOnly(session->{
List<User> userList=session
.createQuery("from User where lower(pushId)=:pushId and id!=:userId")
.setParameter("pushId",pushId.toLowerCase())//忽略大小写
.setParameter("userId",user.getId())
.list();
for (User u:userList){//这个操作,将前人干掉
//更新为null
u.setPushId(null);
session.saveOrUpdate(u);
}
});
if (pushId.equalsIgnoreCase(user.getPushId())){
//如果当前需要绑定的设备Id,之前已经绑定过了
//那么不需要额外绑定
return user;
}else{
//如果当前账户之前的设备id和需要的绑定的不同,那么需要单点登录,让之前的设备退出账户,
//给之前的设备推送一条消息
if (Strings.isNullOrEmpty(user.getPushId())){
//TODO 推送一个退出消息
}
//更新新的设备ID
user.setPushId(pushId);
return Hib.query(session -> {//绑定了设备ID后,更新用户信息
session.saveOrUpdate(user);
return user;
});
}
}
简单分析一下这个方法,就是拿到用户信息,去数据库中查询一下这个pushId有没有被绑定过,有的话,就置空一下。然后更新数据库。如果没有被绑定,那么我们操作当前的User信息,更新pushId,然后存到数据库中去。
进一步封装,让绑定完设备Id后,返回整个用户的信息;
/**
* 绑定设备ID的操作
* @param self
* @param pushId
* @return
*/
private ResponseModel<AccountRspModel> bind(User self, String pushId) {
User user = UserFactory.bindPushId(self, pushId);
if (user == null) {//用户为空直接返回error
return ResponseModel.buildServiceError();
}`在这里插入代码片`
//已经绑定成功
AccountRspModel rspModel = new AccountRspModel(user, true);
return ResponseModel.buildOk(rspModel);
}
接着我们的上层,就可以调用这个bind方法了。一方面Api层,我们肯定是要提供一个手动绑定方法的,此外,为了简化流程,我们在注册登录的时候,也可以提供绑定操作。
那么首先要做的就是在我们的LoginModel和RegisterModel中新增一个属性,用于让客户端传递pushid:
@Expose //简化流程,注册的时候就绑定PushId,可为空,不做校验
private String pushId;
接着在我们的AccountService的login和register方法中调用一下绑定即可:
@POST
@Path("/login")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public ResponseModel<AccountRspModel> post(LoginModel model) {
if (!LoginModel.check(model)) {//对参数进行校验
return ResponseModel.buildParameterError();
}
User user = UserFactory.login(model.getAccount(), model.getPassword());
if (user != null) {
if (!Strings.isNullOrEmpty(model.getPushId())){
return bind(user,model.getPushId());//绑定设备ID
}
//绑定成功返回用户信息
AccountRspModel rspModel=new AccountRspModel(user);
return ResponseModel.buildOk(rspModel);
} else {
return ResponseModel.buildLoginError();
}
}
这里是login的,register的也是在同样的位置。就不累述了。
至此,用户登录注册大体完成,后续总觉得还有优化的点嘞。不过眼下,我们也得提供一个用户信息更新的操作了,毕竟,我滴头像和我滴NickName不能修改的话,也太不人性化了。
更新用户信息
更新用户信息逻辑挺简单,就是传入新的用户信息,使用token验证用户,如果证明是本人,就写入到数据库中。
所以我们在UserFactory中,加个update方法:
public static User update(User user){
return Hib.query(session -> {
session.saveOrUpdate(user);
return user;
});
}
这段代码,看起来是不是很眼熟,每次写入数据库都要用这一段;包括register和login,我们完全可以将他们两个中的数据库操作替换成这个方法。—这里代码就不贴了。
用户更新个人信息的接口,属于用户操作;所以我们在service包下新建一个UserService,用来设计更新接口。
/**
* 用户信息修改接口
* 返回自己的个人信息
* @param model
* @return
*/
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public ResponseModel<UserCard> update(UpdateInfoModel model) {//直接的token都不需要了,拦截里加了
//6-11
User user = getSelf();//能拿到就一定成功了
user = model.updateToUser(user);
user = UserFactory.update(user);
//构造自己的用户信息
UserCard card = new UserCard(user, true);
return ResponseModel.buildOk(card);
}
}
可以看到我们这里的请求方法,用的是PUT,PUT就是进行数据的修改操作。平常用的较多的是GET和POST就是了。
这里的getSelf,就是用来取用户信息的,这个方法稍后会讲。
请求过滤
后台接口设计,还有一个十分重要的部分,就是请求过滤。因为我们肯定不能随便请求一个接口,我们就将用户信息返回给他。这是肯定不行的,我们要对每一次请求都进行一下过滤,看看请求源是否合法,也就是对应的token是否有效。这里有点需要说明,请求过滤并不需要对所有的请求进行过滤,比如,用户登录,注册,这两个接口在调用的时候,用户还没有token,如果直接给过滤掉了,那就是在为难我胖虎。
请求过滤也是一个Provider,这个过滤类需要继承ContainerRequestFilter
,实现其内部的filter方法。过滤的逻辑,实际上就是看一下用户的token是否合法,如果数据库中能查到这个token,说明合理,那么这次请求就可以实现,否则就不行。看一下具体实现:
@Provider
public class AuthRequestFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
//拿到所有的请求接口
String relationPath=((ContainerRequest)requestContext).getPath(false);
//不过滤注册和登陆请求,因为在此之前,用户没有Token值,你过滤了,你就是为难人家
if (relationPath.startsWith("account/login")||
relationPath.startsWith("account/register")){
return;
}
//从Headers中去找第一个token节点
String token=requestContext.getHeaders().getFirst("token");
if (!Strings.isNullOrEmpty(token)){
final User self= UserFactory.findByToken(token);
//如果查询到token信息
if (self!=null){
//给当前请求添加一个上下文
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
//User在这里需要实现Principal接口
return self;
}
@Override
public boolean isUserInRole(String role) {
// TODO: 2020/1/4 ------
//可以在此写入用户的权限,role是权限名
//可以管理管理员权限
return true;
}
@Override
public boolean isSecure() {
//HTTPS
return false;
}
@Override
public String getAuthenticationScheme() {
//不用理会
return null;
}
});
}
//写入上下文后返回
return ;
}
//直接返回一个账户需要登录的model
ResponseModel model=ResponseModel.buildAccountError();
//停止一个请求的继续下发,调用该方法后直接返回请求
//不会走到Service中去
Response response=Response.status(Response.Status.OK)
.entity(model)
.build();
requestContext.abortWith(response);
}
}
可以看到取用户的请求头信息是这个的:
requestContext.getHeaders().getFirst("token");
所以,同理也可以用这个获取其他的参数进行校验,如果有需要的话~~
这里最重要的是:
//给当前请求添加一个上下文
requestContext.setSecurityContext(new SecurityContext() {
通过传入的SecurityContext,我们就可以在请求的地方,拿到当前的上下文;通过这个上下文,我们就可以拿到用户信息。也就是这里:
public Principal getUserPrincipal() {
//User在这里需要实现Principal接口
return self;
}
这里的self实际上就是User这里实体类,User这时候就需要实现Principal接口了。
所以我们就可以写个BaseService。这里面有我们的上下文,然后就可以利用这个上下文,给外界提供一个获取User的方法,供当前的UserService和AccountService获取用户的个人信息:
public class BaseService {
//请求过滤之加上注解,自动识别--具体返回值为我们的拦截器中所返回的SecurityContext
@Context
protected SecurityContext securityContext;
/**
* 从上下文中直接获取信息
* @return
*/
protected User getSelf(){
return (User)securityContext.getUserPrincipal();
}
}
让我们的UserService和AccountService继承自BaseService即可。这也就是我们update方法里调用的那个getSelf.
那么能拿到用户信息了,我们之前的绑定操作里,是不是就可以替换一下了,毕竟当时我们bind方法里用的是findByToken,还要判空什么的。所以改造后的bind方法就是:
@POST
@Path("/bind/{pushId}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public ResponseModel<AccountRspModel> bind(@HeaderParam("token") String token,
@PathParam("pushId") String pushId) {
if (Strings.isNullOrEmpty(token) ||
Strings.isNullOrEmpty(pushId)) {//对参数进行校验
return ResponseModel.buildParameterError();
}
//这里已经拿到用户信息了,不需要多余判断了
User user = getSelf();
return bind(user, pushId);
}
其实这里token可以去掉了,因为我们过滤的时候已经拿到token进行判断了。这里留着纯粹是留个念想。
同其他Provider一样,我们也需要在Application中进行注册:
//请求过滤拦截器
register(AuthRequestFilter.class);
至此,,关于用户账号的后台逻辑可以告一段落了。接下来,就可以看看我们的客户端如何去实现登陆注册了。