Web后端开发_06
SpringBootWeb案例_03
登录认证
1.登录功能
1.1需求
1.2接口文档
1.2.1登录-基本信息
1.2.2请求参数
名称 | 类型 | 是否必须 | 备注 |
---|---|---|---|
username | string | 必须 | 用户名 |
password | string | 必须 | 密码 |
{
"username": "jinyong",
"password": "123456"
}
1.2.3响应数据
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
code | number | 必须 | 响应码, 1 成功 ; 0 失败 | ||
msg | string | 非必须 | 提示信息 | ||
data | string | 必须 | 返回的数据 , jwt令牌 |
{
"code": 1,
"msg": "success",
"data": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTY2MjIwNzA0OH0.KkUc_CXJZJ8Dd063eImx4H9Ojfrr6XMJ-yVzaWCVZCo"
}
1.2.4备注说明
用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,都需要在请求头header中携带到服务端,请求头的名称为 token ,值为 登录时下发的JWT令牌。
{ "code": 0, "msg": "NOT_LOGIN", "data": null }
1.3思路
1.4功能实现
@Slf4j
@RestController
public class LoginController {
@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("用户名或密码错误");
}
}
EmpService.java
员工管理的service的接口
public interface EmpService {
Emp login(Emp emp);
}
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
}
@Mapper
public interface EmpMapper {
/**
* 根据用户名和密码查询员工
*
* @param emp
* @return
*/
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
}
1.5API测试
登录失败:
登录成功:
1.6前端联调
登陆成功
2.登录校验
登录标记
2.1会话技术
- 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
- 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据
- 会话跟踪方案:
2.2会话跟踪方案对比
2.2.1Cookie
示例
@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response) {
response.addCookie(new Cookie("login_username", "Bowen"));//设置Cookie/响应Cookie
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();//获取所有的Cookie
for (Cookie cookie : cookies) {
if (cookie.getName().equals("login_username")) {//输出name为login_username的cookie
System.out.println("login_username:" + cookie.getValue());
}
}
return Result.success();
}
}
cookie1
cookie2
2.2.2Session
示例
@Slf4j
@RestController
public class SessionController {
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());
session.setAttribute("loginUser", "tom"); //往session中存储数据
return Result.success();
}
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2: {}", session.hashCode());
Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}
session1
访问http://localhost:8080/s1
,可以看到响应头多了一行数据
Set-Cookie: JSESSIONID=E6162850B6461136FB3B6E47141BE345; Path=/; HttpOnly
JSESSIONID=E6162850B6461136FB3B6E47141BE345
代表服务器端session对象的ID
session2
访问http://localhost:8080/s2
可以看到请求头中的
Cookie: Idea-28469084=333be614-a4f0-4420-bb00-7ef0ad6f47ab; sidebarStatus=0; login_username=Bowen; JSESSIONID=E6162850B6461136FB3B6E47141BE345
控制台输出的日志中,两次请求拿到的session是同一个,都是295124489
2.2.3令牌技术(主流方案)
2.3JWT令牌
2.3.1JWT(将原始的JSON数据格式进行了安全的封装)
2.3.2场景:登录认证
2.3.3JWT-生成
<!--JWT令牌-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成令牌
@SpringBootTest
class TliasWebManagementApplicationTests {
/**
* 生成JWT令牌
*/
@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, "Bowen")//签名算法
.setClaims(claims)//自定义内容(荷载)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置有效期1h
.compact();
System.out.println(jwt);
}
}
生成的令牌
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTcwMTM1Mzk2NH0.Bn4qfOumxnzuwkFrBxGw3MQa4fdYf8rOCRRUbL02f1M
可以将令牌粘贴到官网进行解析:https://jwt.io/
2.3.4JWT-校验
@SpringBootTest
class TliasWebManagementApplicationTests {
/**
* 解析JWT
*/
@Test
public void testPareJwt() {
Claims claims = Jwts.parser()
.setSigningKey("Bowen")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTcwMTM1MzA3OX0.8dK3aG5KhIjmPd9rxGZ_QW0BXMhWacycD9-jhqW4GKg")
.getBody();
System.out.println(claims);
}
}
2.3.5案例
2.3.5.1接口文档:见1.2接口文档
2.3.5.2JwtUtils.java
工具类
public class JwtUtils {
private static String signKey = "itheima";
private static Long expire = 43200000L;
/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
2.3.5.3LoginController.java
登录的控制层
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
log.info("员工登录:{}", emp);
Emp e = empService.login(emp);
//登录成功,生成令牌,下发令牌
if (e != null) {
Map<String, Object> claims = new HashMap<>();
claims.put("id",e.getId());
claims.put("name",e.getName());
claims.put("username",e.getUsername());
String jwt = JwtUtils.generateJwt(claims);//jwt包含了当前登录的员工信息
return Result.success(jwt);
}
//登录失败,返回错误信息
return Result.error("用户名或密码错误");
}
}
2.3.5.4API测试
用户密码正确时
用户或密码错误时
2.3.5.5前后端联调
登录成功可以抓取去tlias_token
的值eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTcwMTM5NjQyOH0.N9-lpBDXMI_Bj_Bhy0Y6WBx1EcsTxBPA0IDnVqUhMm4
进入员工管理页面,可以找到与tlias_token
中一样的token
2.4过滤器Filter
2.4.1快速入门
- 定义
Filter
:定义一个类,实现Filter
接口,并重写其所有方法。 - 配置
Filter
:Filter类上加@WebFilter
注解,配置拦截资源的路径。引导类上加@ServletComponentScan
开启Servlet
组件支持。
@WebFilter(urlPatterns = "/*")//urlPatterns = "/*" 拦截所有请求
public class DemoFilter implements Filter {
@Override//初始化方法只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
// Filter.super.init(filterConfig);
System.out.println("init 初始化方法执行了");
}
@Override//拦截到请求之后调用,调用多次
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("拦截到了请求");
//放行
filterChain.doFilter(servletRequest, servletResponse);
}
@Override//销毁方法,只调用一次
public void destroy() {
// Filter.super.destroy();
System.out.println("destroy 销毁方法执行了");
}
}
TliasWebManagementApplication.java
启动类
@ServletComponentScan//开启了对servlet组件的支持
@SpringBootApplication
public class TliasWebManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasWebManagementApplication.class, args);
}
}
2.4.2详情(执行流程、拦截路径、过滤器链)
2.4.2.1执行流程
2.4.2.2拦截路径
执行流程搞清楚之后,接下来再来介绍一下过滤器的拦截路径,Filter可以根据需求,配置不同的拦截资源路径:
拦截路径 | urlPatterns值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
示例
拦截了/depts
下的一级路径,(调试的时候,在放行处打断点)
@WebFilter(urlPatterns = "/depts/*")//urlPatterns = "/*" 拦截所有请求
public class DemoFilter implements Filter {
@Override//初始化方法只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}
@Override//拦截到请求之后调用,调用多次
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("拦截到了请求...放行前的逻辑");
//放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("拦截到了请求...放行后的逻辑");
}
@Override//销毁方法,只调用一次
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}
2.4.2.3过滤器链
2.4.2.4示例
@WebFilter("/*")
public class AbcFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Abc拦截到了请求...放行前的逻辑");
//放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("Abc拦截到了请求...放行后的逻辑");
}
}
API测试
2.4.3小结
2.4.4登录校验-Filter
2.4.4.1需求分析
-
在后续的每一次请求当中,都会将JWT令牌携带到服务端,请求到达服务端之后,要想去访问对应的业务功能,此时必须先要校验令牌的有效性。
-
对于校验令牌的这一块操作,使用登录校验的过滤器,在过滤器当中来校验令牌的有效性。如果令牌是无效的,就响应一个错误的信息,也不会再去放行访问对应的资源了。如果令牌存在,并且它是有效的,此时就会放行去访问对应的web资源,执行相应的业务操作。
大概清楚了在Filter过滤器的实现步骤了,那在正式开发登录校验过滤器之前,思考两个问题:
2.4.4.2具体流程
Filter过滤器的流程步骤:
2.4.4.3功能实现
引入阿里巴巴fastJSON依赖
<!--fastJSON-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
LoginCheckFilter.java
登录过滤器
/**
* @ClassName LoginCheckFilter
* @Description 登录过滤器
* @Author Bowen
* @Date 2023/11/30 23:53
* @Version 1.0
**/
@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);//选中代码块后ctrl+alt+t
} catch (Exception e) {//解析失败
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);
}
}
注释掉DemoFilter.java
和AbcFilter
中的过滤器
2.4.4.4API测试
启动springboot,进行测试
令牌eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTcwMTQwNTY1M30.rT2wkW2OoY_UZz75A36ElRVvmQh5ivv214RzXZp3yHk
登录后拿到 员工的urlhttp://localhost:90/#/system/emp
,退出登录,复制该url到地址栏,直接转跳到登录页面。前后端联调成功~~~
2.5拦截器Interceptor
2.5.1简介
2.5.2快速入门
2.5.2.1示例
定义拦截器
在com.bowen
包下创建interceptor.LoginCheckInterceptor.java
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override//目标资源方法运行前,返回true:放行,返回false,不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("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....");
}
}
配置拦截器
在com.bowen
包下创建config.WebConfig.java
配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");
}
}
API测试
使用登录接口测试
控制台日志
控制台日志
2.5.3详解(拦截路径、执行流程)
2.5.3.1拦截路径
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配/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 |
示例1
addPathPatterns("/**").excludePathPatterns("/login")
excludePathPatterns("/login")
排除登录请求
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}
API测试-登录接口
登录返回数据令牌成功,并拿到令牌:eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTcwMTQ1MzM0OX0.EPBM2H_BPFi71Sl4cUs3rzYGuA9xCdsFTPUOxfgNgkY
需要登录后拿到的令牌作为token
查看控制台日志,发现查询部门时拦截后放行,拿到数据,再执行postHandle、afterCompletion方法
查看控制台日志,发现删除部门时拦截后放行,更新数据,再执行postHandle、afterCompletion方法
示例2
addPathPatterns("/*").excludePathPatterns("/login")
addPathPatterns("/*")
拦截一级路径
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/*").excludePathPatterns("/login");
}
}
查看日志,查询部门请求被拦截
API测试-删除部门
查看控制台生成的日志
2.5.3.2执行流程
Filter
与Interceptor
2.5.3.3示例
打开DemoFilter.java
中的过滤器,打开WebConfig.java
中的拦截器,重新启动服务进行测试
API测试-查询部门
查看控制台日志
2.5.4登录校验-Interceptor
2.5.4.1流程图
流程图与Filter过滤器的流程完全一致
2.5.4.2功能实现
WebConfig.java
配置类,在config包下,其中的.excludePathPatterns("/login")
可省略,因为在LoginCheckInterceptor.java
的//> 2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行
,所以造成了重复判断,可删除该拦截器。
/**
* @ClassName WebConfig
* @Description 配置类
* @Author Bowen
* @Date 2023/12/1 10:20
* @Version 1.0
**/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}
LoginCheckInterceptor.java
实现登录校验的拦截器
/**
* @ClassName LoginCheckInterceptor
* @Description 拦截器`Interceptor`演示
* @Author Bowen
* @Date 2023/12/1 10:13
* @Version 1.0
**/
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override//目标资源方法运行前,返回true:放行,返回false,不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
System.out.println("preHandle....");
//> 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);//选中代码块后ctrl+alt+t
} catch (Exception e) {//解析失败
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;
}
@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....");
}
}
2.5.4.3API测试
API测试-登录
API测试-查询部门(未登录)
API测试-查询部门(已登录)
查看日志
前后端联调
启动nginx,不再具体演示,具体操作可参考2.4.4.4API测试
3.异常处理
3.1问题引出
打开浏览器,访问系统中的新增部门操作,系统中已经有了 “就业部” 这个部门,再来增加一个就业部,看看会发生什么现象。
点击确定之后,窗口关闭了,页面没有任何反应,就业部也没有添加上。 而此时会发现,网络请求报错了。
响应回来的数据是一个JSON格式的数据。但这种JSON格式的数据还是我们开发规范当中所提到的统一响应结果Result吗?显然并不是。由于返回的数据不符合开发规范,所以前端并不能解析出响应的JSON数据。
状态码为500,表示服务器端异常,打开idea,查看服务器端出了什么问题。在项目案例中没有进行异常处理。
上述错误信息的含义是,dept部门表的name字段的值 就业部 重复了,因为在数据库表dept中已经有了就业部,之前设计这张表时,为name字段建议了唯一约束,所以该字段的值是不能重复的。
而当再添加就业部,这个部门时,就违反了唯一约束,此时就会报错。
3.2全局异常处理器
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
新建一个包exception
,创建GlobalExceptionHandler.java
全局异常处理器
/**
* @ClassName GlobalExceptionHandler
* @Description 全局异常处理器
* @Author Bowen
* @Date 2023/12/1 16:10
* @Version 1.0
**/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)//捕获所有异常
public Result ex(Exception ex) {
ex.printStackTrace();
return Result.error("对不起操作失败请联系管理员~~~");
}
}
前端返回异常弹窗
3.3总结
@RestControllerAdvice
@ExceptionHandler
原文地址:https://blog.csdn.net/qq_59621600/article/details/134740237
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_49222.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!