1.系统登录实现
表示层:
点击查看代码
@RestController
public class LoginControlly {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login (@RequestBody Map<String,String> map){
String username = map.get("username");
String password = map.get("password");
Emp emp = empService.findByUsernameAndPassword(username,password);
if (emp == null){
return Result.error("用户名不存在密码或错误");
}else {
return Result.success();
}
}
}
业务层:
点击查看代码
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;
@Override
public Emp findByUsernameAndPassword(String username, String password) {
return empMapper.findByUsernameAndPassword(username,password);
}
持久层:
点击查看代码
@Select("select * from emp where username = #{username} and password=#{password}")
Emp findByUsernameAndPassword(String username, String password);
2.登录校验
虽然实现了将用户输入的账号密码与数据库中的数据进行匹配登录,但依然可以不经过登录的情况下,直接输入员工页面地址就可以访问,这是非常不安全的。
-
正确的流程应该是:当访问请求到达服务器后,服务器要校验当前用户是否已经登录过
如果登录过,就放行请求
如果未登录过,就禁止请求访问 -
那如何知道用户是否已经登录过呢?这就需要在用户登录成功后,由服务器为其颁发一个
token(身份标识)
然后用户每次发送请求,都会携带着这个token
而作为系统会对每次的请求进行拦截,校验token的合法性即可
2.1JWT
JWT介绍:
全称:JSON Web Token 用于对应用程序上的用户进行身份标记。
本质上就是一个经过加密处理与校验处理的字符串,它由三部分组成:
头信息(Header):记录令牌类型和签名算法有效载荷(Payload):记录一些自定义能够区分身份的非敏感信息签名(Signature):用于保证Token在传输过程中不被篡改,它是header、payload,加入指定算法计算得来的
登录功能改进
@RestController
public class LoginControlly {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login (@RequestBody Map<String,String> map){
String username = map.get("username");
String password = map.get("password");
Emp emp = empService.findByUsernameAndPassword(username,password);
if (emp == null){
return Result.error("用户名不存在密码或错误");
}else {
//配置载荷
Map<String,Object> claims = new HashMap<>();
claims.put("id",emp.getId());
claims.put("username",emp.getUsername());
claims.put("name",emp.getName());
//生成token令牌
String token = JwtUtils.generateJwt(claims);
return Result.success(token);
}
}
}
修改目前的登录功能,当登录成功后,创建token并返回给客户端
2.2过滤器
当用户访问服务器资源时,过滤器将请求拦截下来,完成一些通用的功能,比如:登录校验、统一编码处理、敏感字符处理等
实现过滤器需要三步
1.定义Filter,定义一个类实现Filter接口,并重写其中的方法。
2.配置Filter: Filter类添加@WebFilter注解,并配置拦截资源的路径。
3.在启动类上加@ServletComponentScan 开启Servlet组件支持。
执行流程
1.客户端向服务器发起访问资源的请求。
2.Filter拦截请求,处理访问资源之前的逻辑。
3.Filter决定是否放行访问请求。
4.请求访问到相关资源,服务器给出响应。
5.Filter拦截响应,处理访问资源之后的逻辑。
6.服务器将响应返回给浏览器。
在代码实现之前,不妨先思考两个问题:
- 所有的请求,拦截到了之后,都需要校验令牌吗?
- 拦截到请求后,在满足什么条件下才可以放行?
对于登录请求来说显然不需要校验令牌,而其他请求就需要了。
拦截到请求后首先要看有没有令牌,有令牌并且令牌没有过期且内容真实就可以放行,否则不放行。
代码实现:
@WebFilter("/*")
public class LoginCheckFilter implements Filter {
@Autowired
private Gson gson;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//1.强转
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse rep = (HttpServletResponse) response;
重写Filter类中的doFilter方法,需要将参数request和response强转为HttpServletRequest类型,方便之后调用方法
//2.处理业务逻辑
//获取url
String uri = req.getRequestURI();
//判断是否含有login
if ("/login".equals(uri)){
//放行
chain.doFilter(req,rep);
return;
}
首先要判断是不是登录请求,如果是则直接放行否则检验令牌,判断的方法为调用getRequestURI方法获取URI判断路径中是否含有"/login"
URI:相当于身份证号 URL:类似于身份证住址+姓名
因此这里使用URI就可以了
//获取请求头中的令牌(token)
String token = req.getHeader("token");
if (token==null){
Result result = Result.error("NOT_LOGIN");
//String json = new ObjectMapper().writeValueAsString(result);
//把java对象转为Json格式
String json = gson.toJson(result);
rep.setContentType("application/json;charset=utf=8");
rep.getWriter().write(json);
return ;
}
如果不是登录就获取请求头中的令牌,通过在浏览器中F12调试可以发现令牌实在请求头中,因此这里调用getHeader方法,判断请求是否带有令牌,如果没有令牌就根据API文档向前端返回"NOT_LOGIN"(两种转换为json格式的方法,谷歌的gson需要导入依赖)
//解析token,解析失败则返回结果
try {
JwtUtils.parseJWT(token);
}catch (Exception e){
//Log.info("token错误");
Result result = Result.error("NOT_LOGIN");
String json = gson.toJson(result);
rep.setContentType("application/json;charset=utf=8");
rep.getWriter().write(json);
return ;
}
//3.放行
chain.doFilter(req,rep);
}
}
最后解析token(令牌),失败则返回结果,成功放行
2.3拦截器
拦截器介绍
拦截器是Spring提供的一种技术,它的功能似于过滤器,它会在进入controller之前,离开controller之后以及响应离开服务时进行拦截。
拦截路径
拦截器的路径写法相对简单,其实只有两个:/*表示一层路径 /**表示多层路径
拦截器实现访问校验
添加依赖
<dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId></dependency>
① 创建Interceptor
判断是否存在token和token是否过期,都没有则返回true放行
@Component
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
@Autowired
private Gson gson;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (token == null){
Result result = Result.error("NOT_LOGIN");
String json = gson.toJson(result);
response.setContentType("application/json;charset-utf-8");
response.getWriter().write(json);
return false;
}
try {
JwtUtils.parseJWT(token);
} catch (Exception e) {
log.info("token错误");
String json = gson.toJson(Result.error("NOT_LOGIN"));
response.setContentType("application/json;charset-utf-8");
response.getWriter().write(json);
return false;
}
return true;
}
}
② 配置Interceptor
实现WebMvcConfigurer接口,重写addInterceptors方法,生成拦截器,调用三个方法
1.addInterceptor 2.addPathPatterns 3.excludePathPatterns
添加拦截器,添加拦截路径,排除的拦截路径
public class MVCConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
3.异常处理
开发一个全局异常处理器来捕获系统中出现的异常,然后给前端一个统一报错
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result hanler(Exception e){
e.printStackTrace();
return Result.error("操作失败");
}
}
4.日志记录
日志记录过程相对繁琐复杂,直接贴代码
@Component
@Aspect
public class LogAspect {
@Pointcut("@annotation(com.itheima.anno.Anno)")
public void pt(){}
@Autowired
//获取token
private HttpServletRequest request;
@Autowired
private LogService logService;
@Around("pt()")
public Object log(ProceedingJoinPoint point) throws Throwable{
Object obj = null;
try {
//1.收集信息
String className = point.getTarget().getClass().getName();
MethodSignature signature = (MethodSignature) point.getSignature();
String methodName = signature.getMethod().getName();
Object[] args = point.getArgs();
//获得方法上的注解的属性值
String methodDesc = signature.getMethod().getAnnotation(Anno.class).methodDesc();
String token = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(token);
Integer id = (Integer) claims.get("id");
long startTime = System.currentTimeMillis();
obj = point.proceed();
long endTime = System.currentTimeMillis();
OperateLog operateLog = new OperateLog();
operateLog.setClassName(className);
operateLog.setMethodName(methodName);
operateLog.setMethodDesc(methodDesc);
operateLog.setMethodParams(Arrays.toString(args));
operateLog.setReturnValue(new ObjectMapper().writeValueAsString(obj));
operateLog.setOperateUser(id);
operateLog.setOperateTime(LocalDateTime.now());
operateLog.setCostTime(endTime-startTime);
//2.插入到表
logService.addLog(operateLog);
} catch (Throwable e) {
e.printStackTrace();
throw new Throwable(e);
}
return obj;
}
}
JAVA注解(Annotation)也称为JAVA标注,是JDK 1.5 引进的一种注释机制。
使用@interface进行创建注解接口,进行注解接口的定义。注解是注解接口声明的简称。
元注解作用范围只适用于注解接口,用于描述注解接口的行为属性。其中比较常用的有4个,分别是@Target、@Retention、@Documented、@Inherited
@Retention 表示注解接口的生存时间。
@Target 描述注解接口的作用域。
ElementType ElementType是一个enum类,其中的enum常量表示作用域范围。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Anno {
String methodDesc();
}