账户开立
1. 前置知识
1.1 MP的使用
1.1.1 引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
1.1.2 修改配置文件
1.1.3 创建实体类
创建包entity,编写实体类AccAgrtTableEntity.java
,并使用lombok简化实体类的编写:
package com.icbc.distributed.accopen.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
/**
*
*
* @author ${author}
* @email ${email}
* @date 2022-07-20 20:37:46
*/
@Data
@TableName("acc_agrt_table")//实现实体类型和数据库中的表实现映射,如AccAgrtTableEntity实体类会映射到数据库acc_agrt_table表
public class AccAgrtTableEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 行号
* 指定实体类属性和表中的主键字段对应。
* MP默认认为id字段是主键列,其他名字的属性MP无法默认自动填充,所以这里想以snId为主键,则需要加@TableId注解
*/
@TableId
private Long snId;
/**
* 账号
*/
private String accId;
/**
* 币种
*/
private Integer currType;
/**
* 利率变动方式;1-固定利率;2-变动利率
*/
private Integer rateIncm;
/**
* 利率变动周期;1-按日变动;2-按月变动;3-按季变动;4-按半年变动;5-按年变动;
*/
private Integer rateAltf;
/**
* 基准利率;有符号数
*/
private Integer baseRate;
/**
* 浮动方式;0-不浮动;2-浮动点数;3-浮动比例;
*/
private Integer floaType;
/**
* 浮动率有符号数
*/
private Integer floaRate;
/**
* 最后计息日期
*/
private Date lastRateDate;
/**
* 最后修改机构
*/
private String lastOrganno;
/**
* 最后修改柜员
*/
private String lastTellerno;
/**
* 最后修改日期
*/
private Date lastModifyDate;
/**
* 地区编号
*/
private Integer regionId;
}
1.1.4 Mapper的CRUD
1.1.4.1 创建mapper/dao接口
创建包mapper/dao
,编写Mapper接口:AccAgrtTableDao.java
:
@Mapper
public interface AccAgrtTableDao extends BaseMapper<AccAgrtTableEntity> {
}
1.1.4.2 启动类添加注解
在Spring Boot启动类中添加@MapperScan
注解,扫描Dao文件夹:
@MapperScan("com.icbc.distributed.accopen.dao")
@EnableDiscoveryClient
@SpringBootApplication()
public class DistributedAccopenApplication {
public static void main(String[] args) {
SpringApplication.run(DistributedAccopenApplication.class, args);
}
}
1.1.4.3 Mapper的通用CRUD测试
实体类:
package com.atguigu.mybatisplus.entity;
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
mapper接口:
package com.atguigu.mybatisplus.mapper;
public interface UserMapper extends BaseMapper<User> {
}
MP中的基本CRUD在内置的BaseMapper中都已得到了实现。
创建MapperTests测试类:
package com.atguigu.mybatisplus;
@SpringBootTest
public class MapperTests {
@Resource
private UserMapper userMapper;
}
1.1.4.3.1 增
@Test
public void testInsert(){
User user = new User();
user.setName("Helen");
user.setAge(18);
//不设置email属性,则生成的动态sql中不包括email字段
int result = userMapper.insert(user);
System.out.println("影响的行数:" + result); //影响的行数
System.out.println("id:" + user.getId()); //id自动回填
}
1.1.4.3.2 删
@Test
public void testDelete(){
int result = userMapper.deleteById(5);
System.out.println("影响的行数:" + result);
}
1.1.4.3.3 改
@Test
public void testUpdate(){
User user = new User();
user.setId(1L);
user.setAge(28);
//注意:update时生成的sql自动是动态sql
int result = userMapper.updateById(user);
System.out.println("影响的行数:" + result);
}
1.1.4.3.4 查
@Test
public void testSelect(){
//按id查询
User user = userMapper.selectById(1);
System.out.println(user);
//按id列表查询
List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
users.forEach(System.out::println);
//按条件查询
Map<String, Object> map = new HashMap<>();
map.put("name", "Helen"); //注意此处是表中的列名,不是类中的属性名
map.put("age", 18);
List<User> users1 = userMapper.selectByMap(map);
users1.forEach(System.out::println);
}
1.1.4.3.5 根据前端表单提交过来的accid去数据库查询acc_id为accid的账号
1.1.4.4 Mapper的自定义CRUD测试
如果MP的BaseMapper内置的CRUD不满足业务需求,需要自定义CRUD时,可以参考Mybatis的做法:写DAO接口,然后在dao.xml中写具体的SQL语句。
1.1.4.5 条件构造器QueryWrapper实现更高级的查询
在MP中我们可以使用通用Mapper(BaseMapper)实现基本查询,也可以使用自定义Mapper(自定义XML)来实现更高级的查询。当然你也可以结合条件构造器来方便的实现更多的高级查询。
new QueryWrapper<AccInfoTableEntity>().eq("acc_id",accid)
:将AccInfoTableEntity
封转成QueryWrapper
对象封装操作类,然后使用QueryWrapper
操作类中的eq
方法去数据库表中查询相应数据。
AccInfoTableEntity实体类:(数据库表acc_info_table的字段信息)
package com.icbc.distributed.accopen.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Data;
/**
*
*
* @author ${author}
* @email ${email}
* @date 2022-07-20 20:37:46
*/
@Data
@TableName("acc_info_table")
public class AccInfoTableEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 行号
*/
// @TableId
private Long snId;
/**
* 账号
*/
@TableId(type = IdType.INPUT)
private String accId;
/**
* 账户状态 0-正常 1-销户
*/
private Integer accStatus;
/**
* 账户名称
*/
private String accTitle;
/**
* 密码
*/
private String password;
/**
* 通存标志
*/
private Integer depositSign;
/**
* 通兑标志
*/
private Integer exchangeSign;
/**
* 存现开关
*/
private Integer depositSwitch;
/**
* 取现开关
*/
private Integer withdrawalSwitch;
/**
* 开立机构
*/
private String openingInstitution;
/**
* 最后修改日期
*/
private Date lastModifiedDate;
/**
* 签订日期
*/
private Date signingDate;
/**
* 注销日期
*/
private Date logoutDate;
/**
* 地区编号
*/
private Integer regionId;
/**
* 支付密码
*/
private String payPassword;
}
继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件及LambdaQueryWrapper, 可以通过 new QueryWrapper().lambda() 方法获取。
queryWrapper.lt()——小于
queryWrapper.le()——小于等于
queryWrapper.gt()——大于
queryWrapper.ge()——大于等于
queryWrapper.eq()——等于
queryWrapper.ne()——不等于
queryWrapper.betweeen(“age”,10,20)——age在值10到20之间
queryWrapper.notBetweeen(“age”,10,20)——age不在值10到20之间
queryWrapper.like(“属性”,“值”)——模糊查询匹配值‘%值%’
queryWrapper.notLike(“属性”,“值”)——模糊查询不匹配值‘%值%’
queryWrapper.likeLeft(“属性”,“值”)——模糊查询匹配最后一位值‘%值’
queryWrapper.likeRight(“属性”,“值”)——模糊查询匹配第一位值‘值%’
queryWrapper.isNull()——值为空或null
queryWrapper.isNotNull()——值不为空或null
queryWrapper.in(“属性”,条件,条件 )——符合多个条件的值
queryWrapper.notIn(“属性”,条件,条件 )——不符合多个条件的值
queryWrapper.or()——或者
queryWrapper.and()——和
queryWrapper.orderByAsc(“属性”)——根据属性升序排序
queryWrapper.orderByDesc(“属性”)——根据属性降序排序
queryWrapper.inSql(“sql语句”)——符合sql语句的值
queryWrapper.notSql(“sql语句”)——不符合SQL语句的值
queryWrapper.esists(“SQL语句”)——查询符合SQL语句的值
queryWrapper.notEsists(“SQL语句”)——查询不符合SQL语句的值
1.1.5 通用Service的CRUD(以转账微服务的AccInfoTableService为例)
MP中有一个接口 IService
和其实现类 ServiceImpl
,封装了常见的业务层逻辑。
1.1.5.1 创建Service接口
package com.icbc.distributed.transfer.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.icbc.common.utils.PageUtils;
import com.icbc.distributed.transfer.entity.AccInfoTableEntity;
import java.util.Map;
/**
* MP中有一个接口 IService和其实现类 ServiceImpl,封装了常见的业务层逻辑
* AccInfoTableService继承了IService接口,那么就继承了该接口中的方法(泛型为AccInfoTableEntity,即对AccInfoTableEntity实体类进行CRUD)
*/
public interface AccInfoTableService extends IService<AccInfoTableEntity> {
// 由于IService没有我们想要的分页功能,所以在该接口也自定义一个分页方法queryPage
PageUtils queryPage(Map<String, Object> params);
}
1.1.5.2 创建Service实现类
创建 impl 包,创建 AccAgrtTableServiceImpl,继承 ServiceImpl,实现 AccAgrtTableService接口:
package com.icbc.distributed.transfer.service.impl;
import org.springframework.stereotype.Service;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.icbc.common.utils.PageUtils;
import com.icbc.common.utils.Query;
import com.icbc.distributed.transfer.dao.AccInfoTableDao;
import com.icbc.distributed.transfer.entity.AccInfoTableEntity;
import com.icbc.distributed.transfer.service.AccInfoTableService;
// ServiceImpl的泛型是<通用mapper, 通用mapper管理的实体>,即<AccInfoTableDao, AccInfoTableEntity>
// 继承 ServiceImpl,默认情况下拥有ServiceImpl下定义的方法;实现AccInfoTableService,目的是扩展自己的业务逻辑
@Service("accInfoTableService")
public class AccInfoTableServiceImpl extends ServiceImpl<AccInfoTableDao, AccInfoTableEntity> implements AccInfoTableService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<AccInfoTableEntity> page = this.page(
new Query<AccInfoTableEntity>().getPage(params),
new QueryWrapper<AccInfoTableEntity>()
);
return new PageUtils(page);
}
}
1.1.5.3 测试
package com.icbc.distributed.transfer.tccservice;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.icbc.common.utils.R;
import com.icbc.distributed.common.IdWorker;
import com.icbc.distributed.transfer.entity.AccInfoTableEntity;
import com.icbc.distributed.transfer.entity.TransferInfoEntity;
import com.icbc.distributed.transfer.service.*;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Service("tccTransaction")
public class TccTransactionService {
@Autowired
private AccInfoTableService accInfoTableService;
@Autowired
IdWorker idWorker;// 分布式自增长ID
public R checkAcc(TransferInfoEntity transferInfoEntity){
Map<String, Object> map = new HashMap<>();
//kay是字段名 value是字段值
map.put("acc_id", transferInfoEntity.getAccIdFrom());
// 4查询账户信息表
// 将数据库中的付方账号和前端表单输入的用户信息进行核对
// 判断借方账号是否存在
List<AccInfoTableEntity> accInfoTableEntityFromList = accInfoTableService.listByMap(map);//根据账号id查询数据库acc_info_table表的信息
if(accInfoTableEntityFromList.size()<1){
return R.error("账号错误, 账号不存在");
}
TransferInfoEntity
实体类:(前端表单传过来的信息)
package com.icbc.distributed.transfer.entity;
import lombok.Data;
import java.util.Date;
@Data
public class TransferInfoEntity {
/**
* 业务唯一编码
*/
private String txnId;
/**
* 执行机构
*/
private String execOrganno;
/**
* 执行柜员
*/
private String execTellerno;
/**
* 币种
*/
private Integer currType;
/**
* 发起 账号
*/
private String accIdFrom;
/**
* 发起 账户名称
*/
private String accTitleFrom;
/**
* 接受 账号
*/
private String accIdTo;
/**
* 接受 账户名称
*/
private String accTitleTo;
/**
* 发生额
*/
private Double amount;
/**
* 交易日期
*/
private Date execDate;
/**
* 支付密码
* */
private String password;
}
AccInfoTableEntity
实体类:(对应数据库表的字段信息)
package com.icbc.distributed.transfer.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
/**
*
*/
@Data
@TableName("acc_info_table")//该实体类对应数据库acc_info_table表
public class AccInfoTableEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 行号
*/
private Long snId;
/**
* 账号
*/
@TableId(value = "acc_id",type = IdType.INPUT)
private String accId;
/**
* 账户状态 0-正常 1-销户
*/
private Integer accStatus;
/**
* 账户名称
*/
private String accTitle;
/**
* 密码
*/
private String password;
/**
* 通存标志
*/
private Integer depositSign;
/**
* 通兑标志
*/
private Integer exchangeSign;
/**
* 存现开关
*/
private Integer depositSwitch;
/**
* 取现开关
*/
private Integer withdrawalSwitch;
/**
* 开立机构
*/
private String openingInstitution;
/**
* 最后修改日期
*/
private Date lastModifiedDate;
/**
* 签订日期
*/
private Date signingDate;
/**
* 注销日期
*/
private Date logoutDate;
/**
* 地区编号
*/
private Integer regionId;
private String payPassword;
}
根据账号id查询数据库acc_info_table表的信息,其中listByMap方法是IService接口自带的方法:
1.1.6 自定义Service接口(以账户开立微服务的LoginAccService为例)
1.1.6.1 添加Service接口方法
public interface LoginAccService {
String loginAcc(AccLoginEntity accLoginEntity);
}
AccLoginEntity
实体类:(前端传过来的)
1.1.6.2 实现Service接口方法
package com.icbc.distributed.accopen.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.icbc.common.exception.MsgCode.BaseCode;
import com.icbc.common.exception.RRException;
import com.icbc.distributed.accopen.dao.AccInfoTableDao;
import com.icbc.distributed.accopen.entity.AccInfoTableEntity;
import com.icbc.distributed.accopen.service.DetectLoginService;
import com.icbc.distributed.accopen.service.LoginAccService;
import com.icbc.distributed.accopen.vo.request.AccLoginEntity;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class LoginAccServiceImpl implements LoginAccService {
@Autowired
AccInfoTableDao accInfoTableDao;
@Autowired
DetectLoginService detectLoginService;
@Override
public String loginAcc(AccLoginEntity accLoginEntity){
String accid=accLoginEntity.getAccid();
// 加密密码
String password= DigestUtils.md5Hex(accLoginEntity.getPassword() + accid);
//1.根据前端表单提交过来的accid去数据库查询acc_id为accid的账号是否存在,不存在则无法登录(判断用户是否存在)
AccInfoTableEntity entity=accInfoTableDao.selectOne(new QueryWrapper<AccInfoTableEntity>()
.eq("acc_id",accid));
if (entity==null){
//没有该账号
// throw new RRException(BaseCode.LOGIN_ACCID_INVAILD_EXCEPTION);
return "账号不存在";
} else {
//2. 账号正确,开始判断账号登录次数
if(!detectLoginService.detectLoginOk(accid)){
return "账号已被锁定,请24小时后重新尝试";
}
// detectLoginService.registLogin( accid, password);
// 3. 根据前端表单提交过来的accid和password去数据库查询acc_id为accid、password为password的账号是否存在,若不存在,则说明前端表单输入的password不正确(登录密码是否正确)
entity=accInfoTableDao.selectOne(new QueryWrapper<AccInfoTableEntity>()
.eq("acc_id",accid)
.eq("password",password));
if(entity==null){
//密码错误
//登录失败记录
int fail = detectLoginService.registLoginError(accid);
return "登录失败,登录失败将锁定账号24小时,剩余"+String.valueOf(fail)+"次。";
// throw new RRException(BaseCode.LOGIN_PASSWORD_INVAILD_EXCEPTION);
}else{
//成功登录记
detectLoginService.registLoginSucces(accid);
return "登录成功";
}
}
}
}
1.1.6.3 测试
package com.icbc.distributed.accopen.controller;
import com.icbc.common.exception.MsgCode.BaseCode;
import com.icbc.common.utils.R;
import com.icbc.distributed.accopen.entity.AccInfoTableEntity;
import com.icbc.distributed.accopen.jwt.JwtUtil;
import com.icbc.distributed.accopen.service.LoginAccService;
import com.icbc.distributed.accopen.vo.request.AccLoginEntity;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
//@RefreshScope
@RestController
@RequestMapping("/AccLogin")
public class AccLoginController {
@Autowired
LoginAccService loginAccService;
@Autowired
JwtUtil jwtUtil;
@RequestMapping("/login")
public R login(@RequestBody AccLoginEntity accLoginEntity){
//loginAccService.loginAcc(accLoginEntity);
String jwt=jwtUtil.createJwt(accLoginEntity.getAccid());
String ret = loginAccService.loginAcc(accLoginEntity);
Map map = new HashMap<String,String>();
if(ret.equals("登录成功")){
map.put("token",jwt);
//map.put("claims",claims);
map.put("code",10009);
return R.ok(map);}
else {
map.put("ret",ret);
map.put("code",10008);
map.put("msg","error");
return R.error(map);}
}
}
1.2 JWT权限控制
1.2.1 单点登录
1.2.2 JWT令牌(用户信息存储在JWT令牌中)
JWT令牌的组成:
该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名。
1.2.3 JWT的用法
客户端接收服务器返回的JWT
,将其存储在Cookie
或localStorage
(本地存储)中。
此后,客户端将在与服务器交互中都会带JWT
。如果将它存储在Cookie
中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP
请求的Header Authorization
字段中。
当跨域时,也可以将JWT
放置于POST
请求的数据主体中。
1.2.3.1 基本依赖
<dependencies>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
1.2.3.2 Jwts.builder()生成token
package com.atguigu.jwt;
public class JwtTests {
//过期时间,毫秒,24小时
private static long tokenExpiration = 24*60*60*1000;
//秘钥
private static String tokenSignKey = "atguigu123";
@Test
public void testCreateToken(){
String token = Jwts.builder()
// 1.JWT头
.setHeaderParam("typ", "JWT") //令牌类型
.setHeaderParam("alg", "HS256") //签名算法
// 2.有效载荷
.setSubject("guli-user") //令牌主题
.setIssuer("atguigu")//签发者
.setAudience("atguigu")//接收者
.setIssuedAt(new Date())//签发时间
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) //过期时间
.setNotBefore(new Date(System.currentTimeMillis() + 20*1000)) //20秒后可用
.setId(UUID.randomUUID().toString())
.claim("nickname", "Helen")
.claim("avatar", "1.jpg")
// 3.签名哈希
.signWith(SignatureAlgorithm.HS256, tokenSignKey)//签名哈希
.compact(); //转换成字符串
System.out.println(token);
}
}
结果:
1.2.3.3 Jwts.parser()解析token
假设要解析这个token
:
@Test
public void testGetUserInfo(){
String token = "jwt字符串";
// Jwts.parser()方法解析token
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey)
.parseClaimsJws(token);
Claims claims = claimsJws.getBody();
String subject = claims.getSubject();
String issuer = claims.getIssuer();
String audience = claims.getAudience();
Date issuedAt = claims.getIssuedAt();
Date expiration = claims.getExpiration();
Date notBefore = claims.getNotBefore();
String id = claims.getId();
System.out.println(subject);
System.out.println(issuer);
System.out.println(audience);
System.out.println(issuedAt);
System.out.println(expiration);
System.out.println(notBefore);
System.out.println(id);;
String nickname = (String)claims.get("nickname");
String avatar = (String)claims.get("avatar");
System.out.println(nickname);
System.out.println(avatar);
}
1.2.4 JWT问题和趋势
- JWT默认不加密,但可以加密。生成原始令牌后,可以使用该令牌再次对其进行加密。
- 当JWT未加密时,一些私密数据无法通过JWT传输。
- JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。
- JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。
- JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行身份验证。
- 为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。
1.2.5 不在生成token的时候设置token的过期时间,而是将token存入redis中,以此解决token过期时间问题
1.2.5 JWT跨域的问题
2. 用户登录
在输入系统的网址后,便可以进入项目的主界面,也就是系统的登录界面。
用户登录时,请求不经过拦截器直接到达controller
,accId
和密码被前端捕获后进行加密操作。随后传入数据层进行数据库查询。
若查询失败,则返回前端失败信息;
若查询成功,则生成token,将accId
和过期时间放入token
并用预先给定的密钥进行签发并返回给前端。
String jwt=jwtUtil.createJwt(accLoginEntity.getAccid());
2.1 JWT做用户认证
2.1.1 微服务模块中添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
2.1.2 JWT工具(添加JwtUtils类)
package com.icbc.distributed.accopen.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtUtil {
@Value("wzt")
private String key;
@Value("3600")
private long ttl;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getTtl() {
return ttl;
}
public void setTtl(long ttl) {
this.ttl = ttl;
}
// 创建Jwt
public String createJwt(String id){
//设置过期时间
ttl=6000000;
long nowm=System.currentTimeMillis();
Date now=new Date(nowm);
/** 设置JWT所存储的信息
* signWith:签名哈希
* setId:jwt的唯一身份标识,主要用来作为一次性token(通过uuid随机生成id),从而避免重放攻击
SignatureAlgorithm.HS256:签名方法 HS256
base64EncodedSecretKey:秘钥
*/
JwtBuilder builder= Jwts.builder().setId(id).signWith(SignatureAlgorithm.HS256,"wzt->123");
// 设置JWT令牌的过期时间
if(ttl>0){
builder.setExpiration(new Date(nowm+ttl));
}
return builder.compact();// 连接成字符串
}
/**
* 解析Jwt字符串
*
* @param jwtStr Jwt字符串
* @return Claims 解析后的对象
*/
public Claims parseJWT(String jwtStr){
return Jwts.parser()
.setSigningKey("wzt->123")
.parseClaimsJws(jwtStr)
.getBody();
}
}
2.2 使用redis缓存登录失败次数(5分钟内一次性登录失败3次,账号会被锁定1天)
RedisTemplate
:
在Spring Boot项目中中,默认集成Spring Data Redis,Spring Data Redis针对Redis提供了非常方便的操作模版RedisTemplate,并且可以进行连接池自动管理。
2.2.1 引入Redis
2.2.1.1 项目中集成Redis
<!-- spring boot redis缓存引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 缓存连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- redis 存储 json序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
2.2.1.2 添加Redis连接配置
application.yml
中添加如下配置:
#spring:
redis:
host: 192.168.100.100 #设置访问的redis数据库的ip地址
port: 6379 #redis安装时设置的默认端口
database: 0 #使用redis中的第1个库
password: 123456 #默认为空
timeout: 5000ms #最大等待时间,超时则抛出异常,否则请求一直等待
lettuce:
pool:
max-active: 20 #最大连接数,负值表示没有限制,默认8
max-wait: -1 #最大阻塞等待时间,负值表示没限制,默认-1
max-idle: 8 #最大空闲连接,默认8
min-idle: 0 #最小空闲连接,默认0
2.2.1.3 启动Redis服务
远程连接Linux服务器:
#启动服务
cd /usr/local/redis-5.0.7
bin/redis-server redis.conf
2.2.2 编写配置类,构造RedisTemplate(使用字符串存储key,使用JSON序列化存储value)
RedisTemplate是用来访问redis数据的模板类。
因为RedisTemplate是<Object,Object>类型的,不利于我们操作,所以要写一个配置类。
2.2.3 使用通过RedisTemplate访问redis,缓存登录失败次数数据、获取数据
每登录失败一次,redis就相应缓存该登录记录。
怎么缓存登录失败次数到redis?
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTests {
//我们需要通过RedisTemplate访问redis,故注入RedisTemplate
@Autowired
private RedisTemplate redisTemplate;
@Test
//访问redis数据库中value为字符串的数据
public void testStrings() {
String redisKey = "test:count";//声明key
redisTemplate.opsForValue().set(redisKey, 1);//存数据,给redisKey这个key赋值为1
System.out.p
3. 校验用户登录
使用拦截器对请求进行拦截,然后使用JWT验证机制对请求头的token进行校验
拦截器配置为拦截除登录注册页面外的所有页面。当前端访问api
接口时(如转账、账户信息查询等),JWT
验证机制可以对接口进行一定的保护。
如果携带的token
经过解析出来的accId
与请求参数中的不一致,说明此用户并未进行登录,此时不能进行后续的操作,需要返回给前端权限不足错误。
package com.icbc.distributed.accopen.filter;
import com.icbc.distributed.accopen.jwt.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*
* 访问接口时验证动作
*/
@Component
public class JwtFilter extends HandlerInterceptorAdapter {
@Autowired
private JwtUtil jwtUtil;
// 1. 根据规则,非登录页面的http请求被拦截器拦截
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println("经过了拦截器");
// 这里只是获取前端传过来的token?并没有校验token(得使用checkToken方法?)?
// 答:这里校验了token了!jwtUtil.parseJWT(token)相当于调用了checkToken方法。
// 2. 拦截器根据密钥对token进行解析,将解析结果放置在请求头内
// 3. 业务模块将token中携带的accId从请求头中取出
// 前端将token放到请求头的,故使用request.getHeader("Authorization")才能获得token,"Authorization"是该token的key
final String token = request.getHeader("Authorization");
if (token!=null) {
// 4. 校验了token是否合法(该语句运行通过,则合法)
Claims claims = jwtUtil.parseJWT(token);
// 5. 将解析结果claims放置在请求头内
request.setAttribute("claims", claims);
}
return true;
}
}
/**
* 解析Jwt字符串
*
* @param jwtStr Jwt字符串
* @return Claims 解析后的对象
*/
public Claims parseJWT(String jwtStr){
return Jwts.parser()
.setSigningKey("wzt->123")
.parseClaimsJws(jwtStr)
.getBody();
}
4. 用户注册与账户开立
点击主界面的开户按钮,便可以进入系统的开户主界面,在主界面中需要输入地区、姓名、手机号、身份证号、登录密码、交易密码等信息。
4.1 账户注册和开户是一起的
账户号为accId,密码为DigestUtils.md5Hex(accOpenEntity.getPassword() + accId)
。
注册流程:
- 首先通过
uuid
工具类生成业务唯一编号,如果重复则循环进行直到得到不重复的uuid
。 - 调用函数将地区名称转为地区编号,网点名称转为网点编号。
- 调用 accId 生成函数(
generateTableId
、generateAccId
方法),根据地区、网点和顺序号计算校验位并生成账号。 - 将生成的账号在数据库中进行查重。
- 更新数据库。
其中,对于3,是这样生成账号ID的:
生成tableId:
生成accId:地区+网点+数据库顺序号
而对于5,需要更新6个数据库表:
数据库更新流程:
- 新增账户信息表,设置账号、密码、开立机构和开立日期等字段;
- 新增业务登记簿,将业务唯一编号、交易日期、操作机构、操作柜员等字段存入;
- 新增账户余额表,添加新开账户的余额和币种信息;
- 新增账户明细表,存入开户过程的操作日志,存入账号、业务唯一编号、币种、交易日期、交易代码等字段。
- 新增存款协议表。
private void updateDateBase(AccOpenEntity accOpenEntity, AccIdTableEntity accIdTableEntity, Date tradeDate, String accId,Long uuid) {
//table2用户信息表
AccInfoTableEntity accInfoTableEntity = new AccInfoTableEntity();
accInfoTableEntity.setAccId(accId);
accInfoTableEntity.setAccStatus(1);
accInfoTableEntity.setAccTitle(accOpenEntity.getAccTitle());
accInfoTableEntity.setPassword(DigestUtils.md5Hex(accOpenEntity.getPassword() + accId));
accInfoTableEntity.setDepositSign(1);
accInfoTableEntity.setExchangeSign(1);
accInfoTableEntity.setDepositSwitch(1);
accInfoTableEntity.setWithdrawalSwitch(1);
accInfoTableEntity.setOpeningInstitution(accOpenEntity.getExecOrganno());
accInfoTableEntity.setLastModifiedDate(tradeDate);
accInfoTableEntity.setSigningDate(tradeDate);
accInfoTableEntity.setPayPassword(DigestUtils.md5Hex(accOpenEntity.getPayPassword()+ accId));
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd");
try{
accInfoTableEntity.setLogoutDate(simpleDateFormat.parse("9999-12-30"));
}catch (ParseException ex){
ex.printStackTrace();
log.error(ex.getMessage());
}
//地区号
accInfoTableEntity.setRegionId(accIdTableEntity.getRegionId());
accInfoTableDao.insert(accInfoTableEntity);
//table3业务登记簿
BizRegTableEntity bizRegTableEntity=new BizRegTableEntity();
bizRegTableEntity.setTxnDate(tradeDate);
bizRegTableEntity.setTxnId(String.valueOf(uuid));
bizRegTableEntity.setChannelType(1);
bizRegTableEntity.setExecOrganno(accOpenEntity.getExecOrganno()); //操作机构
bizRegTableEntity.setExecTellerno(accOpenEntity.getExecTellerno());
bizRegTableEntity.setTxnCode(1);
bizRegTableEntity.setTxnType(2);
bizRegTableEntity.setCashTransferFlag(1);
bizRegTableEntity.setDebitAccId("null");
bizRegTableEntity.setDebitAccName("null");
bizRegTableEntity.setCreditCurrType(0);
bizRegTableEntity.setCreditAmount(0l);
bizRegTableEntity.setStatus(0);
bizRegTableEntity.setLastModifyDate(tradeDate);
bizRegTableEntity.setRegionId(accIdTableEntity.getRegionId());//地区号
bizRegTableEntity.setDebitCurrType(0);
bizRegTableEntity.setDebitAmount(0l);
bizRegTableEntity.setCreditAccId("null");
bizRegTableEntity.setCreditAccName("null");
bizRegTableDao.insert(bizRegTableEntity);
//table4新增账户余额
AccBalanceTableEntity accBalanceTableEntity =new AccBalanceTableEntity();
accBalanceTableEntity.setAccId(accId);
accBalanceTableEntity.setCurrType(0);
accBalanceTableEntity.setCurBalance(0l);
accBalanceTableEntity.setYdayBalance(0l);
accBalanceTableEntity.setLastTxnDate(tradeDate);
accBalanceTableEntity.setRegionId(accIdTableEntity.getRegionId());//调用公共层方法地区转地区号
accBalanceTableDao.insert(accBalanceTableEntity);
//table5新增账户明细表
AccDetailsTableEntity accDetailsTableEntity=new AccDetailsTableEntity();
accDetailsTableEntity.setDepartId(1);//分区编号
accDetailsTableEntity.setAccId(accId);
accDetailsTableEntity.setCurrType(0);
accDetailsTableEntity.setTxnId(String.valueOf(uuid));
accDetailsTableEntity.setExecOrganno(accOpenEntity.getExecOrganno()); //操作机构 ,调用公共层方法操作机构转操作机构号
accDetailsTableEntity.setExecTellerno(accOpenEntity.getExecTellerno());
accDetailsTableEntity.setExecDate(tradeDate);
accDetailsTableEntity.setTxnCode(0);
accDetailsTableEntity.setCashFlag(1);
accDetailsTableEntity.setLoanFlag(0);
accDetailsTableEntity.setAmount(0l);
accDetailsTableEntity.setBalance(0l);
accDetailsTableEntity.setBalance(0l);
accDetailsTableEntity.setRegionId(accIdTableEntity.getRegionId());//地区号
accDetailsTableDao.insert(accDetailsTableEntity);
//table6新增存款协议表
AccAgrtTableEntity accAgrtTableEntity=new AccAgrtTableEntity();
accAgrtTableEntity.setAccId(accId);
accAgrtTableEntity.setCurrType(0);
accAgrtTableEntity.setRateIncm(1);
accAgrtTableEntity.setRateAltf(5);
accAgrtTableEntity.setBaseRate(0);
accAgrtTableEntity.setFloaType(0);
accAgrtTableEntity.setFloaRate(0);
accAgrtTableEntity.setLastRateDate(tradeDate);
accAgrtTableEntity.setLastOrganno(accOpenEntity.getExecOrganno()); //操作机构 ,调用公共层方法操作机构转操作机构号
accAgrtTableEntity.setLastTellerno(String.valueOf(accOpenEntity.getExecTellerno()));
accAgrtTableEntity.setLastModifyDate(tradeDate);
accAgrtTableEntity.setRegionId(accIdTableEntity.getRegionId());//地区号
accAgrtTableDao.insert(accAgrtTableEntity);
}