Project(2)——用户注册业务层、控制器层、前端页面&附:密码加密

Project(2)

1、分析项目

当需要开发某个项目时,首先,应该分析这个项目中,需要处理哪些种类的数据!例如:用户、商品、商品类别、收藏、订单、购物车、收货地址…

然后,将以上这些种类的数据的处理排个顺序,即先处理哪种数据,后处理哪种数据!通常,应该先处理基础数据,再处理所相关的数据,例如需要先处理商品数据,才可以处理订单数据,如果多种数据之间没有明显的关联,则应该先处理简单的,再处理较难的!

则以上这些数据的处理顺序应该是:用户 > 收货地址 > 商品类别 > 商品 > 收藏 > 购物车 > 订单

当确定了数据处理顺序后,就应该分析某个用户对应的功能有哪些,以“用户”数据为例,相关功能有:注册、登录、修改密码、修改资料、上传头像…

然后,还是需要确定以上功能的开发顺序,通常,遵循“增 > 查 > 删 > 改”的顺序,则以上功能的开发顺序应该是:注册 > 登录 > 修改密码 > 修改资料 > 上传头像。

每个功能的开发都应该遵循 创建数据表 > 创建实体类 > 持久层 > 业务层 > 控制器层 > 前端页面

一次只解决一个问题
大问题拆成小问题

2、用户 - 注册 - 创建数据表

3、用户 - 注册 - 创建实体类

4、用户 - 注册 - 持久层

4.1.规划SQL语句
4.2.接口与抽象方法
4.3.配置映射

5、用户 - 注册 - 业务层

5.1.业务层的基本定位

在MVC设计理念中,M表示的是Model,即数据模型,它由持久层和业务层共同构成!持久层负责完成数据操作,即增删改查,业务层负责组织业务流程和管理业务逻辑,业务更多在表现为用户能操作的某1个“功能”,但是,对于程序员来说,可能是更多的细小的功能来组成的!之所以需要业务层,是因为需要通过业务层来保证数据的安全,(数据必须经过业务层的各种流程和逻辑才产生或发生变化!)

5.2.规划异常

考虑当前的“注册”功能可能会抛出哪些异常!

在“注册”时,需要先检查用户名是否被占用,如果已经被占用,则不允许注册,就是一种“操作失败”,则应该有“用户名占用异常”被抛出!

如果用户名没有被占用,则允许注册,将执行数据表的INSERT操作,凡是数据表的增、删、改操作,都是有可能出现操作失败的(磁盘已满、MySQL服务器崩了等等)!则应该抛出“插入数据异常”。

在业务层,一旦视为“操作失败”,对异常的处理都应该是“抛出”,而不是自行处理!因为业务层不适合进行处理!因为业务层不会直接和用户交互,直接和用户交互的是控制器,所以应该异常抛出,抛给调用业务层的控制器,由控制器处理或继续抛出。

此次,需要创建2个异常类:
cn.tedu.store.service.ex.UsernameDuplicateException
cn.tedu.store.service.ex.InsertException
另外还需创建这两个异常的公共父级异常类cn.tedu.store.service.ex.ServiceException
他们都应该是RuntimeException的子孙类(所有的自定义异常类都应该是RuntimeException的子孙类):

RuntimeException
    ServiceException
        UsernameDuplicateException
        InsertException
        ......
package cn.tedu.store.service.ex;

/**
 * 业务异常,是业务层抛出的异常的基类
 */
public class ServiceException extends RuntimeException {
	
	private static final long serialVersionUID = 1L;

	public ServiceException() {
		super();
	}

	public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
		super(message, cause, enableSuppression, writableStackTrace);
	}

	public ServiceException(String message, Throwable cause) {
		super(message, cause);
	}

	public ServiceException(String message) {
		super(message);
	}

	public ServiceException(Throwable cause) {
		super(cause);
	}
	
	
	
}

/**
 * 用户名冲突异常,如:用户名已经被占用
 */
public class UsernameDuplicateException extends ServiceException {
    //实现序列化接口版本号
    //实现父类的五个构造方法,同上
}
/**
 * 插入数据异常
 */
public class InsertException extends ServiceException {
    //实现序列化接口版本号
    //实现父类的五个构造方法,同上
}

5.3.接口与抽象方法
在设计业务层时,应该先有业务层的接口,并在接口中定义抽象方法,后续,外界(当前Model以外,例如controller或其它service等)调用时,是基于接口来声明对象,并调用方法的,所以,此处使用接口是一种解耦的做法!

先创建cn.tedu.store.service.IUserService业务层接口,并添加抽象方法:

//一般RuntimeException及其子孙类异常不必在方法中声明出来,而其他的IO类异常必须声明。
//此处为了更直观地了解到reg方法可能抛出的异常,所以声明了出来。
void reg(User user) throws UsernameDuplicateException, InsertException;
/**
 * 处理用户数据的业务层接口
 */
public interface IUserService {
	
	/**
	 * 用户注册
	 * @param user 用户数据
	 * @throws UsernameDuplicateException 用户名冲突异常
	 * @throws InsertException 插入数据异常
	 */
	void reg(User user) throws UsernameDuplicateException, InsertException;
	
}

关于业务层的抽象方法的设计原则:

  1. 返回值:仅以操作成功为前提来设计返回值(因为通常失败会以异常的方式返回给用户,不必考虑失败的返回值);
  2. 方法名:尽量体现业务例如使用regregistregister表示注册,使用login表示登录;
  3. 参数列表:一定是客户端可以提供的数据,或来自于Session中的数据,且足够调用持久层的各方法;
  4. 异常:把用户操作失败的可能,都设计成各种异常,并把这些异常都添加到方法的声明中。

5.4.实现类与重写方法

创建cn.tedu.store.service.impl.UserServiceImpl业务层实现类,
实现以上IUserService接口,
添加@Service注解,
并在类中声明@Autowired private UserMapper userMapper持久层对象:

    @Service
    public class UserServiceImpl implements IUserService {
	
	    @Autowired
	    private UserMapper userMapper;

    }

然后,重写接口中的抽象方法:

    @Override
	public void reg(User user) throws UsernameDuplicateException, InsertException {
		// 根据参数user对象中的username属性查询数据
		// userMapper.findByUsername
		// 判断查询结果是否不为null(用户名已存在)
		// 是--用户名已被占用(抛出UsernameDuplicateException)
		
		// TODO 得到盐值
		// TODO 基于参数user对象中的password进行加密,得到加密后的密码
		// TODO 将加密后的密码和盐值封装到user中
		
		// 将user中的isDelete设置为0
		
		// 封装user中的4个日志属性
		
		// 执行注册:userMapper.insert(user)
	}

代码实现:

@Service
public class UserServiceImpl implements IUserService {

	@Autowired
	private UserMapper userMapper;

	@Override
	public void reg(User user) throws UsernameDuplicateException, InsertException {
		// 根据参数user对象中的username属性查询数据
		// userMapper.findByUsername
		String username = user.getUsername();
		User result = userMapper.findByUsername(username);
		// 判断查询结果是否不为null(用户名已存在)
		if (result != null) {
			// 是--用户名已被占用(抛出UsernameDuplicateException)
			throw new UsernameDuplicateException("注册失败!用户名已经被占用!");
		}
		
		System.err.println("reg() > password = " + user.getPassword());
		// 得到盐值
		String salt = UUID.randomUUID().toString();
		// 基于参数user对象中的password进行加密,得到加密后的密码
		String md5Password = getMd5Password(user.getPassword(), salt);
		// 将加密后的密码和盐值封装到user中
		user.setPassword(md5Password);
		user.setSalt(salt);
		System.err.println("reg() > salt = " + salt);
		System.err.println("reg() > md5Password = " + md5Password);

		// 将user中的isDelete设置为0
		user.setIsDelete(0);

		// 封装user中的4个日志属性
		Date now = new Date();
		user.setCreatedUser(username);
		user.setCreatedTime(now);
		user.setModifiedUser(username);
		user.setModifiedTime(now);

		// 执行注册:userMapper.insert(user)
		Integer rows = userMapper.insert(user);
		if (rows != 1) {
			throw new InsertException("注册失败!出现未知错误!请联系管理员!");
		}
	}

	/**
	 * 对密码进行加密
	 * 
	 * @param password 原始密码
	 * @param salt     盐值
	 * @return 加密后的密码
	 */
	String getMd5Password(String password, String salt) {
		// 规则:对 原始密码+盐值 3重加密
		String str = password + salt;
		for (int i = 0; i < 3; i++) {
			str = DigestUtils.md5Hex(str.getBytes());
		}
		return str;
	}

}

注意:特别是在执行插入数据时,业务层除了需要保证业务流程和业务逻辑,还需要保证数据的完整性!

完成后,在src/test/java下创建cn.tedu.store.service.UserServiceTests测试类,编写并执行单元测试:

@SpringBootTest
public class UserServiceTests {
	
	@Autowired
	private IUserService service;
	
	@Test
	public void testReg() {
		try {
			User user = new User();
			user.setUsername("Tom3");
			user.setPassword("123");
			service.reg(user);
			System.err.println("OK");
		} catch (ServiceException e) {
			System.err.println(e.getClass().getName());
			System.err.println(e.getMessage());
		}
	}
	
}

6、用户 - 注册 - 控制器层

6.1.处理异常

首先,需要创建响应结果的数据类型cn.tedu.store.util.JsonResult

package cn.tedu.store.util;

/**
 * 向客户端响应操作结果的数据类型
 * 
 * @param <T> 向客户端响应的数据的类型
 */
public class JsonResult<T> {

	private Integer state;
	private String message;
	private T data;

	public Integer getState() {
		return state;
	}

	public void setState(Integer state) {
		this.state = state;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

	public T getData() {
		return data;
	}

	public void setData(T data) {
		this.data = data;
	}
    
    public JsonResult(Integer state) {
		this.state = state;
	}
	
	public JsonResult() {	
	}

}

在编写处理请求的控制器及方法之前,还应该对异常进行统一处理!

在统一处理异常时,相关代码只能作用于当前控制器类,为了使得整个项目都可以使用这个统一处理异常的机制,应该把相应代码添加在控制器类的基类cn.tedu.store.controller.BaseController中,所以:

/**
 * 控制器类的基类,实现统一处理异常
 * @author DELL
 *
 */
public abstract class BaseController {
	/**
	 * 操作结果的“成功”状态
	 */
	public static final Integer SUCCESS = 2000;
	
	//只处理ServiceException及其子孙类异常,避免异常过度处理
	@ExceptionHandler(ServiceException.class)
	public JsonResult<Void> handleException(Throwable e){
		JsonResult<Void> jr = new JsonResult<Void>();
		jr.setMessage(e.getMessage());
		
		if(e instanceof UsernameDuplicateException) {
			jr.setState(4000);
		}else if(e instanceof InsertException) {
			jr.setState(5000);
		}
		
		return jr;
	}
	
}

6.2.设计请求
需要事先规划用户的请求是什么样的,例如用户向哪个URL发出请求表示“注册”,请求方式是哪种,是否需要提交某些请求参数等,以及最终服务器端向客户端响应什么样的数据:

请求路径:/users/reg
请求参数:User user
请求类型:POST
响应数据:JsonResult<Void>

6.3.处理请求

创建cn.tedu.store.controller.UserController控制器类,添加@RestController@RequestMapping("users")注解,并在类中添加@Autowired private IUserService userService;业务层对象。

关于@RestController注解:

SpringBoot框架推荐服务器端不必关心页面的处理,在处理请求时,应该响应正文,在对控制器类添加注解时,可以使用@RestController,这个注解既能起到@Controller的作用,还能表示该控制器类所有的方法都是响应正文的!也就是说,使用了这个注解,该类中每个方法都相当于添加了@ResponseBody注解!如果一定要转发或重定向,则不可以使用@RestController,只能继续使用@Controller注解!

并在类中添加处理请求的方法:

/**
 * 处理用户数据相关请求的控制器类
 * @author DELL
 *
 */
@RestController
@RequestMapping("users")
public class UserController extends BaseController {
	
	@Autowired
	private IUserService userService;
	
	@RequestMapping("reg")
	public JsonResult<Void> reg(User user){
		// 执行注册,失败时会抛出异常,异常会在BaseController类中进行统一处理
		// 此方法只处理返回成功的情况。
		userService.reg(user);
		//返回成功
		return new JsonResult<Void>(SUCCESS);
	}
	
}

处理请求的方法中添加@RequestMapping("reg")注解是为了方便在地址栏中进行测试:
打开浏览器,输入http://localhost:8080/users/reg?username=Tom4&password=123进行测试。

7、用户 - 注册 - 前端页面

    <script type="text/javascript">
	    // 页面加载完成后执行的方法(函数)
		$(document).ready(function() {
			// 在button控件上添加id:btn-reg
			$("#btn-reg").click(function() {
				$.ajax({
					// url:      请求交到哪里去
					// data:     请求提交的参数,以上的form表单中,在需要提交的参数的控件中添加name属性;
					//            form表单上添加id属性
					// type:     请求方式
					// dataType: 服务器端响应的结果的类型
					// success:  响应成功时的处理函数
					"url" : "/users/reg",
					"data" : $("#form-reg").serialize(),
					"type" : "post",
					"dataType" : "json",
					"success" : function(json) {
						if (json.state == 2000) {
							alert("注册成功!");
							//跳转到某个页面
						} else {
							alert(json.message);
						}
					}
				});
			});
		});
	</script>

8、用户 - 登录 - 持久层

9、用户 - 登录 - 业务层

10、用户 - 登录 - 控制器层

11、用户 - 登录 - 前端页面

-------------------------------------------------------------------------------

附1:密码加密

加密算法并不适合对密码进行加密!因为,所以的加密算法都是可以逆运算的,如果使用的算法和加密时使用的参数是已知的,则可以轻松推算出原始数据!而密码安全问题主要源自内部泄密!也就是说,数据库的数据能泄密,可以认为算法和加密参数也是有泄密的可能的!

对于密码安全问题而言,最好的做法就是将密码进行加密,却任何人都无法解密!

则,在密码加密这个问题上,所有的加密算法都是不使用的!只能使用摘要算法!

摘要算法的全称是“消息摘要算法”!这种算法的特征有:

  1. 消息原文相同时,得到的摘要是相同的;
  2. 使用的摘要算法没有发生变化时,无论原文长度是多少,摘要的长度是固定的;
  3. 消息原文不同时,得到的摘要几乎不会相同!

在消息摘要领域中,一定存在N种不同的原文,可以计算得到完全相同的摘要!

使用MD5(Message Digest 5)算法执行摘要运算时,得到的结果都是32位的十六进制数。转换成二进制就需要128位,所以,MD5算法也成为128位算法!

如果有2个不同的原文,可以得到相同的摘要,这种状况称之为“碰撞”,在有限长度的原文中,得到相同的摘要的概率是极低,甚至就是不可能的!

所以,真正用于对密码加密的算法,都是摘要算法,主要原因是因为它不可逆,且在有限长度的原文的基础之上,几乎不可能发生碰撞!

常见的摘要算法有MD5家族和SHA系列,例如:MD2、MD4、MD5、SHA1、SHA128、SHA256、SHA384、SHA512.….其中,MD家族的都是128的,SHA系列名称后有位数的,就是对应的位数。

关于MD5或相关摘要算法的破解研究是存在的,主要是针对算法的碰撞攻击,而并不是尝试执行逆运算来得到原始数据!

另外,还有许多网站号称可以在线破解MD5,只需要将摘要结果填进去,就可以查到原始数据,例如填入e10adc3949ba59abbe56e057f20f883e就可以查出123456,事实上,这些网站是记录了大量的原始数据与MD5摘要数据的对应关系,如果原始数据比较简单,或者是常用密码值,被这些网站收录的可能性就非常大!所以,为了提升密码的安全性,保证密码不被这些网站“破解”,可以采取的做法:

  1. 增强原始密码的安全强度,例如从组成元素、长度方面提出更高要求;
  2. 反复执行加密,即多重加密;
  3. 加盐;
  4. 综合以上所有的做法。
SELECT 原始数据 FROM 表 WHERE 摘要数据=?

要实现MD5算法可以使用spring自带的工具类,也可以添加新的依赖:

        <dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
		</dependency>

MD5算法:

    @Test
	public void messageDigest() {
		String password = "000000";
		//随机盐值
		String salt = UUID.randomUUID().toString(); 
		
		System.err.println(salt);
		password = password + salt;
		
		//md5算法:
		String md5 = DigestUtils.md5DigestAsHex(password.getBytes());
		System.err.println(md5);
	}
"123456"
e10adc3949ba59abbe56e057f20f883e

"1"
c4ca4238a0b923820dcc509a6f75849b

"1111111111111111111111111111111111"
67cc4ac459440ed68504c334a03baa7f
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值