登录功能:
Controller:
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("员工登录: {}",emp);
Emp e = empService.login(emp);
return e != null?Result.success():Result.error("用户名或密码错误");
}
ServiceImpl:
public Emp login(Emp emp){
return empMapper.getByUsernameAndPassword(emp);
}
Mapper:
@Select("selelct * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
登陆标记:
用户登陆成功之后,每一次请求中,都可以获取到该标记
统一拦截:
- 过滤器Filter (servlet规范)
- 拦截器Interceptor (spring提供)
会话技术:
- 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应
- 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据
会话跟踪方案:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
- 令牌技术
方案对比:
-
Cookie:
优点:HTTP协议中支持的技术
缺点:
- 移动端APP无法使用Cookie
- 不安全,用户可以自己禁用Cookie
- Cookie不能跨域
跨域区分为三个维度:协议、IP/域名、端口;只要三个维度中有任意维度不同,那就是跨域操作
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
- Session:
优点:存储在服务端,安全
缺点:
- 服务器集群环境下无法直接使用Session
- Cookie的缺点
- 令牌技术:
优点:
- 支持PC端、移动端
- 解决集群环境下的认证问题
- 减轻服务器存储压力
缺点:
需要自己实现
JWT:
全称:JSON Web Token(http://jwt.io/)
定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的
组成:
- 第一部分(base64编码):Header(头),记录令牌类型、签名算法等。例如:{“alg”:”HS256″,”type”:”JWT”}
- 对json字符串进行base64编码
-
base64编码:是一种基于64个可打印字符(A-Z,a-z,0-9,+,/)来表示二进制数据的编码方式
- 第二部分(base64编码):Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{“id”:”1″,”username”:”Tom”}
-
第三部分(不是base64 ):Signatrue(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定密钥,通过指定签名算法计算而来,根据前面的内容自动计算而来,并不是base64
引入依赖:
io.jsonwebtoken
jjwt
0.9.1 生成JWTS:
—————————————————–
@Test
public void testGenJwt(){
Map<String, Object> claims = new HashMap<>();
claims.put(“id”,1);
claims.put(“name”,”tom”);String jwt = Jwts.builder() .signWith(SignatureAlgorithm.HS256, "itheima")//(签名算法,"密钥") .setClaims(claims) //自定义内容(载荷) .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置有效期为1h .compact();//调用compact()之后会拿到一个字符串类型的返回值,返回值存到jwt System.out.println(jwt);
}
解析JWTS:
—————————————————–
public void testParseJwt(){
Claims claims = Jwts.parser()
.setSigningKey(“itheima”)//指定签名密钥
//解析令牌
.parseClaimsJws(“eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY3MDQ2NDU0N30.yPLRyiusrlrmWeC4-dhInjFuAghPkmiHSRHd_DTKi9E”)
.getBody();
System.out.println(claims);
}
—————————————————–
注意事项:
- JWT校验时使用的签名密钥,必须和生成JWT令牌时使用的密钥的配套的
- 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法
过滤器(Filter):
概念:Filter过滤器,是javaWeb三大组件(Servlet、Filter、Listener)之一。
- 过滤器可以把资源的请求拦截下来,从而实现一些特殊的功能
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等
快速入门:
- 定义Filter:定义一个类:实现Filter接口,并重写其所有方法
- 配置Filter:Filter类加上@WebFilter注解,配置拦截资源的路径。启动类上加@ServletComponentScan开启Servlet组件支持
@WebFilter(urlPatterns = "/*") public class DemoFilter implements Filter { @Override //初始化方法, Web服务器启动,创建Filter时调用,只调用一次 public void init(FilterConfig filterConfig) throws ServletException { System.out.println("init 初始化方法执行了"); } @Override //拦截到请求之后调用, 可调用多次 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("Demo 拦截到了请求...放行前逻辑"); //放行 chain.doFilter(request,response); System.out.println("Demo 拦截到了请求...放行后逻辑"); } @Override //销毁方法, 服务器关闭时调用,只调用一次 public void destroy() { System.out.println("destroy 销毁方法执行了"); } }
注意:
- 重写实现方法时,idea 默认只实现 doFilter,因为init和destroy方法并不常用,Filter接口中已经提供默认实现
- 放行后访问对应资源,资源访问完成后,还会回到Filter中
- 回到Filter中,执行放行之后的逻辑
Filter拦截路径:
- 拦截具体路径:” /login “,只有访问 /login 路径时,才会被拦截
-
目录拦截: ” /emps/* “,访问 /emps 下的所有资源,都会被拦截
-
拦截所有:” * “,访问所有资源,都会被拦截
过滤器链:
一个Web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链
顺序:注解配置的Filter,优先级是按照过滤器类名(子字符串)的自然排序
登录校验Filter:
1、获取请求url
2、判断请求url中是否包含login,如果包含,说明是登录操作,放行
3、获取请求头中的令牌(token)
4、判断令牌是否存在,如果不存在,返回错误结果(未登录)
5、解析token,如果解析失败,返回错误结果(未登录)
6、放行
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//1.获取请求url。
String url = req.getRequestURL().toString();
log.info("请求的url: {}",url);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){
log.info("登录操作, 放行...");
chain.doFilter(request,response);
return;
}
//3.获取请求头中的令牌(token)。
String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//5.解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//jwt解析失败
e.printStackTrace();
log.info("解析令牌失败, 返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//6.放行。
log.info("令牌合法, 放行");
chain.doFilter(request, response);
}
}
拦截器(Interceptor):
概念:
是一种动态截取方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行
作用:
拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码
Interceptor快速入门:
- 定义拦截器,实现HandlerIntercrptor接口,并重写其所有方法
- 注册拦截器
@Component public class LoginCheckInterceptor implements HandlerInterceptor { @Override //目标资源方法运行前运行, 返回true: 放行, 放回false, 不放行 public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { System.out.print("preHandle..."); return true; @Override //目标资源方法运行后运行 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle ..."); } @Override //视图渲染完毕后运行, 最后运行 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion..."); } } ------------------------------------------------------- @Configuration //配置类 public class WebConfig implements WebMvcConfigurer { @Autowired private LoginCheckInterceptor loginCheckInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //配置拦截路径, "/**" 表示拦截所有 registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**"); } }
拦截器-拦截路径:
- 拦截器可以根据需求,配置不同的拦截路径:
- ” /* “:一级路径,能匹配/depts,/emps,/login,不能匹配/depts/1
- ” /** “:任意级路径,能匹配depts,/depts/1,depts/1/2
- ” /depts/* “:/depts 下的一级路径,能匹配 /depts/1,不能匹配 /depts/1/2, /depts
- ” /depts/** “: /depts下的任意级路径,能匹配/depts,/depts/1,depts/1/2,不能匹配/emps/1
@Override public void addInterceptors(InterceptorRegistry registry) { //配置拦截路径 // addPathPatterns("/**")表示拦截所有 // excludePathPatterns("/login") 表示不需要拦截哪些资源 registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); }
拦截器-执行流程:
1、浏览器->Filter->DispatcherServlet->Interceptor->Controller
2、浏览器<-Filter<-DispatcherServlet<-Interceptor<-Controller
Filter 与 Interceptor:
- 接口规范不同:
- 过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口
- 拦截范围不同:
- 过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源
登录校验Interceptor:
1、获取请求url
2、判断请求url中是否包含login,如果包含,说明是登录操作,放行
3、获取请求头中的令牌(token)
4、判断令牌是否存在,如果不存在,返回错误结果(未登录)
5、解析token,如果解析失败,返回错误结果(未登录)
6、放行
@Override //目标资源方法运行前运行, 返回true: 放行, 放回false, 不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
//1.获取请求url。
String url = req.getRequestURL().toString();
log.info(“请求的url: {}”,url);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){
log.info("登录操作, 放行...");
return true;
}
//3.获取请求头中的令牌(token)。
String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//5.解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//jwt解析失败
e.printStackTrace();
log.info("解析令牌失败, 返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//6.放行。
log.info("令牌合法, 放行");
return true;
}
异常处理:
- 方案一:在Controller的每一个方法中进行 try…catch 处理,代码臃肿,不推荐
- 方案二:全局异常处理器,简单,优雅,推荐
抛异常:Mapper->Service->Controller->全局异常处理器
//@RestControllerAdvice = @ControllerAdvice + @ResponseBody
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)//捕获所有异常
public Result ex(Exception ex){
ex.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
}
@RestControllerAdvice 加在类上;
@ExceptionHandler 加在方法上,通过这个注解指定当前方法要捕获哪一类型的异常
事务管理:
概念:
事务是一组操作的集合,它是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败
操作:
1、开启事务(一组操作开始前,开启事务):start transaction / begin ;
2、提交事务(这组操作全部成功后,提交事务):commit ;
3、回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;
例:
解散部门:删除部门,同时删除该部门下的员工
yml:
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
Controller:
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) throws Exception {
log.info("根据id删除部门:{}",id);
//调用service删除部门
deptService.delete(id);
return Result.success();
}
ServiceImpl:
@Transactional
@Autowired
private EmpMapper empMapper;
@Override
public void delete(Integer id) throws Exception {
deptMapper.deleteById(id); //根据ID删除部门数据
empMapper.deleteByDeptId(id); //根据部门ID删除该部门下的员工
}
Mapper:
//根据部门ID删除该部门下的员工数据
@Delete("delete from emp where dept_id = #{deptId}")
void deleteByDeptId(Integer deptId);
Spring事务管理:
注解:@transaction
位置:业务(service)层的方法上、类上、接口上
作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
rollbackFor:
默认情况下,只有出现RuntimeException才回滚异常。 rollvackFor 属性用于控制出现何种异常类型,回滚事务
@Transactional(rollvackFor = Exception.class)//表示所有异常都会回滚
propagation:
事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制
*REQUIRED:默认值,需要事务,有则加入,无则创建新事务
*REQUIRED_NEW:需要新事务,无论有无,总是创建新事务
SUPPORTS:支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED:不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY:必须有事务,否则抛异常
NEVER:必须没事务,否则抛异常
例:
解散部门时,无论是成功还是失败,都要记录操作日志
步骤:
1、解散部门:删除部门、删除部门下的员工
2、记录日志到数据库表中
DeptServiceImpl:
@Autowired
private DeptLogService deptLogService;
@Transactional
@Override
public void delete(Integer id) throws Exception {
try {
deptMapper.deleteById(id); //根据ID删除部门数据
int i = 1/0;
//if(true){throw new Exception("出错啦...");}
empMapper.deleteByDeptId(id); //根据部门ID删除该部门下的员工
} finally {
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此次解散的是"+id+"号部门");
deptLogService.insert(deptLog);
/*
public interface DeptLogService {
void insert(DeptLog deptLog);
}
*/
}
}
DeptLogServiceImpl:
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
/*
@Mapper
public interface DeptLogMapper {
@Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
void insert(DeptLog log);
}
*/
@Transactional(propagation = Propagation.REQUIRES_NEW) //需要新事物,无论有无,总是创建新事物
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}