我们使用 Spring Boot
从零开始实现一个 TODO
项目,实现的项目,不包含真实上线的流程。
开发环境
- MacBook Air - Sonoma 14.2
- IntelliJ IDEA 2021.2.2
- Google Chrome - 版本 120.0.6099.129(正式版本) (arm64)
- Navicat Premium - 16.0.12
- Postman - Version 8.12.1
项目搭建
我们先创建项目。
New Project
- Atifact 填写 - todo-service
- Group 填写 - com.jimmy
- Java 选择版本 17
Go next.
- Spring Boot 选择 3.2.1
Dependencies 选择如下:
- Spring Web
- Lombok
- Spring Data JPA
- MySQL Driver
添加 mysql
在 Navicat Premium
中新建数据库:
- 数据库名:todo_service
- 字符集:utf8mb4
- 排序规则:utf8mb4_general_ci
And then.
在生成的项目中 src/main/resources/application.properties
文件中,添加下面的内容:
bash
spring.datasource.url=jdbc:mysql://localhost:3306/todo_service
spring.datasource.username=root
spring.datasource.password=
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update
运行项目
执行项目运行,控制台没有报错,项目运行成功。
创建 Controller
我们简单创建一个 Controller
的案例 Demo
,来了解过程。
创建 controller 的 Demo
在项目 src/main/java/com/jimmy.todoservice
下,创建包,其名为 controller
。
And then.
在包 controller
下,创建类,其名为 Demo
,写入如下的代码:
java
package com.jimmy.todoservice.controller;
@RestController
@RequestMapping("api")
public class Demo {
@GetMapping("/hello")
public String sayHello() {
return "Hello World!";
}
}
验证
运行项目后,在 postman
上执行接口:
bash
method [GET]
url [http://localhost:8080/api/hello]
能够正确输出 Hello World!
的信息。
数据写入 MySql
真实的项目,我们需要有自己的数据库来存储数据。这里,我们选择了 MySql
。
添加数据库表映射
项目进入 src/main/java/com.jimmy.todoservice
下创建包 entity
。
And then.
然后在包 entity
下创建类 Demo
,然后填写下面的内容:
java
package com.jimmy.todoservice.entity;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "demo")
public class Demo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false)
private String name;
}
此时,如果我们执行项目,则会创建名为 demo
的数据表。我们可以进入 Navicat Premium
中数据库 todo_service
下查看表 demo
,该表内有两个字段,分别为 id
和 name
。
添加对应的 TDO
我们创建了 entity
,下面我们创建相关的 tdo
,方便前端数据的写入。
我们在 src/main/java/com.jimmy.todoservice
下创建包,名为 dto
。
And then.
在包 dto
下面创建类,名为 DemoDto
,添加下面的内容:
java
package com.jimmy.todoservice.dto;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class DemoDto {
private Long id;
private String name;
}
添加对应的 Repository 来与数据库建立联系
在 src/main/java/com.jimmy.todoservice
下创建包,名为 repository
。
And then.
在包 repository
下创建接口类文件,名为 DemoRepository
,添加下面的内容:
java
package com.jimmy.todoservice.repository;
public interface DemoRepository extends JpaRepository<Demo, Long> {
}
创建对应的服务 service
在 src/main/java/com.jimmy.todoservice
下创建包,名为 service
。
And then.
在包 service
中创建接口类 DemoService
,并添加下面的内容:
java
package com.jimmy.todoservice.service;
public interface DemoService {
// Add demo item
DemoDto addDemoItem(DemoDto demoDto);
}
在包 service
下面创建包 impl
。
And then.
在 service/impl
下创建类 DemoServiceImpl
,并添加下面的内容:
java
package com.jimmy.todoservice.service.impl;
@Service
@AllArgsConstructor
public class DemoServiceImpl implements DemoService {
private DemoRepository demoRepository;
@Override
public DemoDto addDemoItem(DemoDto demoDto) {
return null;
}
}
在完善上面 DemoServiceImpl.java
文件之前,我们先添加 modelMapper
,用于 dto
和 entity
数据的转换。
添加 modelMapper
我们在 pom.xml
中,添加下面的依赖:
xml
<!-- https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.2.0</version>
</dependency>
安装上面的依赖后,在入口文件 TodoServiceApplication
中添加下面的内容:
java
@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.STRICT); // https://stackoverflow.com/questions/58838964/modelmapper-failed-to-convert-java-lang-string-to-java-lang-long
return modelMapper;
}
整个文件 TodoServiceApplication.java
的内容(移除了 import
引入)如下:
java
package com.jimmy.todoservice;
@SpringBootApplication
public class TodoServiceApplication {
// 使用 model mapper
@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.STRICT); // https://stackoverflow.com/questions/58838964/modelmapper-failed-to-convert-java-lang-string-to-java-lang-long
return modelMapper;
}
public static void main(String[] args) {
SpringApplication.run(TodoServiceApplication.class, args);
}
}
OK,我们返回上面的 service/impl/DemoServiceImpl.java
文件。
完善 DemoSeriveImpl
我们引入 modelMapper
,整个文件的内容(移除了 import
引入)如下:
java
package com.jimmy.todoservice.service.impl;
@Service
@AllArgsConstructor
public class DemoServiceImpl implements DemoService {
private DemoRepository demoRepository;
private ModelMapper modelMapper;
@Override
public DemoDto addDemoItem(DemoDto demoDto) {
Demo demo = modelMapper.map(demoDto, Demo.class);
Demo savedDemo = demoRepository.save(demo);
DemoDto savedDemoDto = modelMapper.map(savedDemo, DemoDto.class);
return savedDemoDto;
}
}
添加对应的 controller 操作
我们在之前的 src/main/java/com.jimmy.totoservice/controller/Demo.java
文件内,添加下面的 add
接口操作。整个文件的内容(移除了 import
引入)如下:
java
package com.jimmy.todoservice.controller;
@RestController
@RequestMapping("api")
@AllArgsConstructor
public class Demo {
private DemoService demoService;
@GetMapping("/hello")
public String sayHello() {
return "Hello World!";
}
@PostMapping("/add")
public ResponseEntity<DemoDto> addName(@RequestBody DemoDto demoDto) {
DemoDto savedDemoDto = demoService.addDemoItem(demoDto);
return new ResponseEntity<>(savedDemoDto, HttpStatus.OK);
}
}
验证
我们运行项目起来。在 postman
上执行下面的接口:
bash
method [POST]
url [http://localhost:8080/api/add]
body -> {"name": "jimmy"}
查看返回的写入数据库的结果。
我们打开 Navicat Premium
查看 todo_service
数据库中表 demo
写入了新数据。
信息返回
我们统一处理返回的信息。
公共返回文件
我们在 src/main/java/com.jimmy.todoservice
下新建包 common
,然后在其下面新建类 ResultData
,内容如下:
java
package com.jimmy.todoservice.common;
@Data
public class ResultData<T> {
private String code;
private String message;
private T data;
// 扩展字段,比如接口的请求时间
private Long accessTimestamp;
// private String path; // TODO: 获取请求的路径
// 构造函数
public ResultData() {
this.accessTimestamp = System.currentTimeMillis();
}
// 成功返回
public static <T> ResultData<T> success(T data) {
ResultData<T> resultData = new ResultData<>();
resultData.setCode("10000");
resultData.setMessage(("请求成功!"));
resultData.setData(data);
return resultData;
}
// 失败返回
public static <T> ResultData<T> fail(String code, String message) {
ResultData<T> resultData = new ResultData<>();
resultData.setCode(code);
resultData.setMessage(message);
return resultData;
}
}
Demo
然后,我们更改 Get
接口请求,在文件 controller/Demo
下更改:
java
@GetMapping("/get/{id}")
public ResultData<DemoDto> getItem(@PathVariable("id") Long id) {
DemoDto demoDto = demoService.getDemoItem(id);
return ResultData.success(demoDto);
}
验证
启动项目,在 postman
上进行验证:
bash
method [GET]
url [http://localhost:8080/api/get/1]
添加 security - 注册和登录
我们引入 security
进行验证。
安装依赖
在项目根目录的 pom.xml
文件中添加下面的依赖引用:
xml
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
此时,运行项目,在浏览器中打开链接 http://localhost:8080
会自动跳转到登陆的页面。账号为默认 user
,密码是随机生成的,可见于控制台 Using generated security password:
后的一串字符串。
自定义用户名和密码
当然,我们也可以自定义用户名和密码,我们在文件 src/main/resources/application.properties
中添加:
bash
spring.security.user.name=jimmy
spring.security.user.password=123456
重新启动项目后,我们可以通过用户名/密码 jimmy/123456
来登陆。
用户注册
下面,我们实现一个系统用户注册。
首先,我们先配置 spring security config
配置类。在 com.jimmy.todoservice/config
下添加下面的内容:
java
package com.jimmy.todoservice.config;
@Configuration
@EnableMethodSecurity
@AllArgsConstructor
public class SpringSecurityConfig {
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf((csrf) -> csrf.disable())
.authorizeHttpRequests((authorize) -> {
authorize.requestMatchers("/api/auth/**").permitAll();
authorize.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll();
authorize.anyRequest().authenticated();
}).httpBasic(Customizer.withDefaults());
return httpSecurity.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
我们在 com.jimmy.todoservice/entity
下添加用户类 User
,内容如下:
java
package com.jimmy.todoservice.entity;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
}
然后在 com.jimmy.todoservice/dto
下添加 RegisterDto
类:
java
package com.jimmy.todoservice.dto;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class RegisterDto {
private String name;
private String username;
private String email;
private String password;
}
在 com.jimmy.todoservice/repository
下添加类 UserRepository
,内容如下:
java
package com.jimmy.todoservice.repository;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsernameOrEmail(String username, String email);
}
然后在 com.jimmy.todoservice/service
下添加接口类 AuthService
,内容如下:
java
package com.jimmy.todoservice.service;
public interface AuthService {
String register(RegisterDto registerDto);
String login(LoginDto loginDto);
}
这里我把登陆的接口也罗列出来了。
下面是注册用户重点👇
我们实现注册接口,在 com.jimmy.todoservice/service/impl
下添加下面的内容:
typescript
package com.jimmy.todoservice.service.impl;
@Service
@AllArgsConstructor
public class AuthServiceImpl implements AuthService {
private UserRepository userRepository;
@Override
public String register(RegisterDto registerDto) {
User user = new User();
user.setName(registerDto.getName());
user.setUsername(registerDto.getUsername());
user.setEmail(registerDto.getEmail());
user.setPassword(registerDto.getPassword());
userRepository.save(user);
return null;
}
@Override
public String login(LoginDto loginDto) {
// 登陆验证
return null;
}
}
这里我们只是简单的将用户信息,密码还是明文,写入到数据库中。
这个时候,我们在 com.jimmy.todoservice/controller
下添加类 AuthController
,内容如下:
java
package com.jimmy.todoservice.controller;
@AllArgsConstructor
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private AuthService authService;
// 注册接口
@PostMapping("/register")
public String register(@RequestBody RegisterDto registerDto) {
String response = authService.register(registerDto);
System.out.println(response);
return "register";
}
// 登陆接口
@PostMapping("/login")
public String login(@RequestBody LoginDto loginDto) {
return "login";
}
}
运行项目,我们在 Postman
上直接调用注册接口,进行注册。
bash
[POST] http://localhost:8080/api/auth/register
[BODY] {
"name": "嘉明",
"username": "jimmy",
"email": "reng99@outlook.com",
"password": "123456"
}
执行后,通过 Navicat
进入数据库查看,发现内容写入。
嗯,这里刚才也说了,我们的用户密码是明文写入,这样很不安全,我们更改下注册类 com.jimmy.todoservive/service/impl/AuthServiceImpl.java
,更改后内容如下:
java
package com.jimmy.todoservice.service.impl;
@Service
@AllArgsConstructor
public class AuthServiceImpl implements AuthService {
private PasswordEncoder passwordEncoder; // 密码加密
private UserRepository userRepository;
@Override
public String register(RegisterDto registerDto) {
User user = new User();
user.setName(registerDto.getName());
user.setUsername(registerDto.getUsername());
user.setEmail(registerDto.getEmail());
user.setPassword(passwordEncoder.encode(registerDto.getPassword()));
userRepository.save(user);
return null;
}
@Override
public String login(LoginDto loginDto) {
// 登陆验证
return null;
}
}
重新运行项目,在 Navicat
上删除存在的数据。我们在 Postman
上直接调用注册接口,进行注册。
bash
[POST] http://localhost:8080/api/auth/register
[BODY] {
"name": "jimmy",
"username": "jimmy",
"email": "reng99@outlook.com",
"password": "123456"
}
执行后,通过 Navicat
进入数据库查看,发现内容写入,此时的密码是加密的。
用户登录
我们在 com.jimmy.todoservice/security
下添加类 CustomUserDetailsService
来重写 security
的方法 loadUserByUsername
,初步的内容如下:
java
package com.jimmy.todoservice.security;
@Service
@AllArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
System.out.println("username or email");
System.out.println(usernameOrEmail);
// 从数据库中查询
User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow(() -> new UsernameNotFoundException("该用户不存在"));
System.out.println(user);
return null;
}
}
我们在 com.jimmy.todoservice/dto
下添加类 LoginDto
,内容如下:
java
package com.jimmy.todoservice.dto;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class LoginDto {
private String usernameOrEmail;
private String password;
}
我们在上面的 controller
包下面的类 AuthController
已经添加了登陆的 api
了。我们执行下看看:
bash
[POST] http://localhost:8080/api/auth/login
[BODY] {
"usernameOrEmail": "reng99@outlook.com",
"password": "123456"
}
重启项目后,我们并不能进行登陆,只是 return "login";
。
下面我们来更改👇
更改com.jimmy.todoservice/controller/AuthController.java
:
java
// 登陆接口
@PostMapping("/login")
public String login(@RequestBody LoginDto loginDto) {
String token = authService.login(loginDto);
return token;
}
更改 com.jimmy.todoservice/service/impl/AuthServiceImpl.java
:
java
@Override
public String login(LoginDto loginDto) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
loginDto.getUsernameOrEmail(),
loginDto.getPassword()
);
// 登陆验证
try {
Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) {
e.printStackTrace();
}
return "token successfully return";
}
更改 com.jimmy.todoservice/security/CustomUserDetailsService.java
内容:
java
package com.jimmy.todoservice.security;
@Service
@AllArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
// 从数据库中查询
User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow(() -> new UsernameNotFoundException("该用户不存在"));
// Set<GrantedAuthority> authorities = null; // null 会报错,不允许空
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER")); // 模拟,写死
return new org.springframework.security.core.userdetails.User(
usernameOrEmail,
user.getPassword(),
authorities
);
}
}
最后,我们重新启动项目。在 Postman
上请求:
bash
[POST] http://localhost:8080/api/auth/login
[BODY] {
"usernameOrEmail": "reng99@outlook.com",
"password": "123456"
}
能够成功返回信息,验证通过✅。
至此,我们可以把 src/main/resources/application.properties
文件内设定的账号密码移除:
bash
spring.security.user.name=jimmy // -
spring.security.user.password=123456 // -
整合 JWT
TODO 项目,整合 JWT。
安装依赖
在项目根目录中,文件 pom.xml
中添加依赖:
xml
<!-- jwt -->
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
安装上面的依赖。
Jwt 生成和校验
我们在 src/main/resources/application.properties
内添加内容,如下:
bash
# jwt - sha256 -> jimmy https://emn178.github.io/online-tools/sha256.html https://www.unitconverters.net/time/millisecond-to-day.htm
app.jwt-secret=930a68a51a2db950f58fd3b0b5f1d76f56afaa16e12a418d71ca6c25f2390424
app.jwt-expiration-milliseconds=604800000
上面添加了私钥和过期时间
然后,我们在 src/main/java/com.jimmy.todoservice/security
中添加下面三个文件:
JwtTokenProvider.class
生成 token
,校验 token
:
java
package com.jimmy.todoservice.security;
@Component
public class JwtTokenProvider {
// 密钥
@Value("${app.jwt-secret}")
private String jwtSecret;
// 过期时间
@Value("${app.jwt-expiration-milliseconds}")
private long jwtExpirationDate;
// 生成 JWT token
public String generateToken(Authentication authentication) {
String username = authentication.getName();
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);
String token = Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(expireDate)
.signWith(key())
.compact();
return token;
}
private Key key() {
return Keys.hmacShaKeyFor(
Decoders.BASE64URL.decode(jwtSecret)
);
}
// 从 token 中获取用户名
public String getUsername(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key())
.build()
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
return username;
}
// 验证 JWT token
public boolean validateToken(String token) {
// 注意: token should not be empty
Jwts.parserBuilder()
.setSigningKey(key())
.build()
.parse(token);
return true;
}
}
JwtAuthenticationEntryPoint.class
重写认证的入口:
java
package com.jimmy.todoservice.security;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { // 认证入口文件
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
JwtAuthenticationFilter.class
对每个请求进行过滤:
java
package com.jimmy.todoservice.security;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter { // 对请求进行过滤
private JwtTokenProvider jwtTokenProvider;
private UserDetailsService userDetailsService;
// 判断 token 是否为空
private boolean isTokenEmpty(String token) {
return token == null || token.trim() == "";
}
// 从请求中,获取 token
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// Get JWT token form HTTP request
String token = getTokenFromRequest(request);
// Validate Token
if(!isTokenEmpty(token) && StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
// 从 token 中获取用户名
String username = jwtTokenProvider.getUsername(token);
// 获取 security 的用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
}
那么,我们接下来在登陆接口中生成 token
。我们进入文件 com.jimmy.todoservice/service/impl/AuthServiceImpl.java
中改写登陆的接口实现:
java
@Override
public String login(LoginDto loginDto) {
// 登陆验证
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
loginDto.getUsernameOrEmail(),
loginDto.getPassword()
);
Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成 token
String token = jwtTokenProvider.generateToken(authentication);
return token;
}
然后,重启项目。通过 Postman
请求:
bash
[POST] http://localhost:8080:/api/auth/login
[BODY] {
"usernameOrEmail": "reng99@outlook.com",
"password": "123456"
}
Postman
中接口返回 token
信息。验证通过✅
Jwt 校验 Demo
我们在上面已经完成了 Jwt
的校验。那么,下面,我们来实现其他接口需要 jwt
校验的案例。
我们先来实现一个获取用户列表的 api
,我们添加的内容如下👇
我们在 com.jimmy.todoservice/dto
下添加类 UserDto
:
java
package com.jimmy.todoservice.dto;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private String name;
private String username;
private String email;
private String password;
}
然后在 com.jimmy.todoservice/service
包下添加接口类 UserService
:
java
package com.jimmy.todoservice.service;
public interface UserService {
// 获取用户列表数据
List<UserDto> getAllUsers();
}
在 com.jimmy.todoservice/service/impl
下添加该接口实现类 UserServiceImpl
:
java
package com.jimmy.todoservice.service.impl;
@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
private ModelMapper modelMapper;
@Override
public List<UserDto> getAllUsers() {
List<User> users = userRepository.findAll();
return users.stream().map((user) -> modelMapper.map(user, UserDto.class))
.collect(Collectors.toList());
}
}
接着,我们在 com.jimmy.service/controller
包下添加类 UserController
:
java
package com.jimmy.todoservice.controller;
@AllArgsConstructor
@RestController
@RequestMapping("/api/users")
public class UserController {
private UserService userService;
// 获取所有用户列表
@GetMapping
public ResponseEntity<List<UserDto>> getAllUsers() {
List<UserDto> users = userService.getAllUsers();
return new ResponseEntity<>(users, HttpStatus.OK);
}
}
至此,我们启动项目,通过 Postman
进行接口调用:
bash
[GET] http://localhost:8080/api/users
在没有添加 token 得情况下会出现 401
,添加了登陆的 token
就会获取到用户的列表。
但是,接口并没有通过。
我们来重新整改下之前配置的内容,下面👇
更改 com.jimmy.todoservice/config/SpringSecurityConfig.java
:
java
package com.jimmy.todoservice.config;
@Configuration
@EnableMethodSecurity
@AllArgsConstructor
public class SpringSecurityConfig {
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf((csrf) -> csrf.disable())
.authorizeHttpRequests((authorize) -> {
authorize.requestMatchers("/api/auth/**").permitAll();
authorize.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll();
authorize.anyRequest().authenticated();
}).httpBasic(Customizer.withDefaults());
// 添加内容 -> 认证, jwt
httpSecurity.exceptionHandling(exception -> exception
.authenticationEntryPoint(jwtAuthenticationEntryPoint));
httpSecurity.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
更改 com.jimmy.todoservice/security/JwtAuthenticationFilter.java
文件:
java
package com.jimmy.todoservice.security;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter { // 对请求进行过滤
private JwtTokenProvider jwtTokenProvider;
private UserDetailsService userDetailsService;
// 添加内容 -> 构造函数
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, UserDetailsService userDetailsService) {
this.jwtTokenProvider = jwtTokenProvider;
this.userDetailsService = userDetailsService;
}
// 判断 token 是否为空
private boolean isTokenEmpty(String token) {
return token == null || token.trim() == "";
}
// 从请求中,获取 token
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// Get JWT token form HTTP request
String token = getTokenFromRequest(request);
// Validate Token
if(!isTokenEmpty(token) && StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
// 从 token 中获取用户名
String username = jwtTokenProvider.getUsername(token);
// 获取 security 的用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
}
重新运行项目。
通过 Postman
测试,能够成功返回登陆接口的 token
信息。并且带 token
信息访问用户的列表接口,能够返回用户列表信息数据;不带 token
访问用户列表接口,则返回 401
。
角色表增删改查
TODO 项目,进行角色限制。
初始内容
在上面添加 security - 注册和登录 小节中,我们在 com.jimmy.todoservice/security/CustomUserDetailsService.java
模拟了角色授权。
java
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER")); // 模拟,写死
下面,我们新建表来进行关联。
在进行关联之前,先对角色表进行增删改除 - 这个也是本文的重点。用户表和角色表的关联放在下一篇文章。
添加 entity
,在 com.jimmy.todoservice/entity
包下添加类 Role
:
java
package com.jimmy.todoservice.entity;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
添加 repository
,在 com.jimmy.todoservice/repository
包下添加类 RoleRepository
:
java
package com.jimmy.todoservice.repository;
public interface RoleRepository extends JpaRepository<Role, Long> {
Role findByName(String name);
}
添加 dto
,在 com.jimmy.todoservice/dto
包下添加类 RoleDto
:
java
package com.jimmy.todoservice.dto;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class RoleDto {
private String name;
}
添加 service
,在 com.jimmy.todoservice/service
包下添加接口类 RoleService
:
scss
package com.jimmy.todoservice.service;
public interface RoleService {
// 添加角色
RoleDto addRole(RoleDto roleDto);
// 获取角色
RoleDto getRole(Long id);
// 获取列表
List<RoleDto> getAllRoles();
// 删除角色
RoleDto deleteRole(Long id);
}
添加服务实现,在 com.jimmy.todoservice/service/impl
包下添加对应的实现类 RoleServiceImpl
,下面是文件骨架内容:
java
package com.jimmy.todoservice.service.impl;
@Service
@AllArgsConstructor
public class RoleServiceImpl implements RoleService {
@Override
public RoleDto addRole(RoleDto roleDto) {
return null;
}
@Override
public RoleDto getRole(Long id) {
return null;
}
@Override
public List<RoleDto> getAllRoles() {
return null;
}
@Override
public RoleDto deleteRole(Long id) {
return null;
}
}
角色的添加
在 com.jimmy.todoservice/service/impl/RoleServiceImpl.java
中更改 addRole
方法:
java
private RoleRepository roleRepository;
private ModelMapper modelMapper;
@Override
public RoleDto addRole(RoleDto roleDto) {
Role role = modelMapper.map(roleDto, Role.class);
Role savedRole = roleRepository.save(role);
RoleDto savedRoleDto = modelMapper.map(savedRole, RoleDto.class);
return savedRoleDto;
}
我们添加对应的 controller
,在 com.jimmy.todoservice/controller
下添加类 RoleController
:
java
package com.jimmy.todoservice.controller;
@RestController
@RequestMapping("/api/roles")
@AllArgsConstructor
public class RoleController {
private RoleService roleService;
// 添加角色
@PostMapping
public ResponseEntity<RoleDto> addRole(@RequestBody RoleDto roleDto) {
RoleDto savedRoleDto = roleService.addRole(roleDto);
return new ResponseEntity<>(savedRoleDto, HttpStatus.CREATED);
}
}
然后,我们启动项目。通过 Postman
请求接口:
bash
[POST] http://localhost:8080/api/roles
[BODY] {
"name": "ADMIN"
}
注意:前提要登陆,带上 token 请求上面的接口
请求成功后,通过 Navicat
查看 roles
表中,数据已经正确写入。
获取角色列表
在 com.jimmy.todoservice/service/impl/RoleServiceImpl.java
中更改 getAllRoles
方法:
java
@Override
public List<RoleDto> getAllRoles() {
List<Role> roles = roleRepository.findAll();
return roles.stream().map(role -> modelMapper.map(role, RoleDto.class))
.collect(Collectors.toList());
}
我们添加对应的 controller
,在 com.jimmy.todoservice/controller/RoleController.java
中添加接口如下:
java
// 获取角色列表
@GetMapping
public ResponseEntity<List<RoleDto>> getAllRoles() {
List<RoleDto> roles = roleService.getAllRoles();
return new ResponseEntity<>(roles, HttpStatus.OK);
}
重新运行项目。在 Postman
中请求下面的接口:
bash
[GET] http://localhost:8080/api/roles
注意:前提要登陆,带上 token 请求上面的接口
请求成功,放回数据列表。
获取指定 ID 角色
我们先添加个错误提示先,在 com.jimmy.todoservice/exception
下添加类 ResourceNotFoundException
:
java
package com.jimmy.todoservice.exception;
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException{
public ResourceNotFoundException(String message) {
super(message);
}
}
更改 com.jimmy.todoservice/service/impl/RoleServiceImpl.java
文件下的 getRole
方法:
java
@Override
public RoleDto getRole(Long id) {
Role role = roleRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("资源找不到"));
return modelMapper.map(role, RoleDto.class);
}
接着编写 controller
,在 com.jimmy.todoservice/controller/RoleController.java
文件中添加接口:
java
// 获取指定 id 的角色
@GetMapping("/{id}")
public ResponseEntity<RoleDto> getRole(@PathVariable("id") Long roleId) {
RoleDto roleDto = roleService.getRole(roleId);
return new ResponseEntity<>(roleDto, HttpStatus.OK);
}
此时启动项目,通过 Postman
调用接口:
bash
[GET] http://localhost:8080/api/roles/1
注意:前提要登陆,带上 token 请求上面的接口
成功返回信息。
这个时候,我们返回的信息没有 id
字段。那是因为我们在 RoleDto
中没有编写 id
,我们改写下 com.jimmy.todoservice/dto/RoleDto.java
文件内容:
java
package com.jimmy.todoservice.dto;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class RoleDto {
private long id;
private String name;
}
然后重新启动项目,调用接口:
bash
[GET] http://localhost:8080/api/roles/1
此时有 id
的字段信息返回。
更改指定 ID 角色
在此之前,我们添加个 description
字段在 entity
中,这步忽略。读者自行更改。
我们在 com.jimmy.todoservice/service/RoleService.java
上添加实现的方法:
java
// 更新角色
RoleDto updateRole(Long id, RoleDto roleDto);
然后在 com.jimmy.todoservice/service/impl/RoleServiceImpl.java
内添加 updateRole
方法的实现:
java
@Override
public RoleDto updateRole(Long id, RoleDto roleDto) {
Role role = roleRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("资源找不到"));
if(roleDto != null && roleDto.getName() != null && !roleDto.getName().isEmpty()) {
role.setName(roleDto.getName());
}
if(roleDto != null && roleDto.getDescription() != null && !roleDto.getDescription().isEmpty()) {
role.setDescription(roleDto.getDescription());
}
Role updatedRole = roleRepository.save(role);
return modelMapper.map(updatedRole, RoleDto.class);
}
接着,我们添加相关的 controller
,在 com.jimmy.todoservice/controller/RoleController.java
上添加接口:
java
// 更新指定 id 的角色
@PutMapping("/{id}")
public ResponseEntity<RoleDto> updateRole(@PathVariable("id") Long roleId, @RequestBody RoleDto roleDto) {
RoleDto savedRoleDto = roleService.updateRole(roleId, roleDto);
return new ResponseEntity<>(savedRoleDto, HttpStatus.OK);
}
启动项目,在 Postman
上测试接口:
bash
[PUT] http://localhost:8080/api/roles/1
[BODY] {
"description": "管理员"
}
注意:前提要登陆,带上 token 请求上面的接口
进入 Navicat
中查看记录被成功修改。
删除指定 ID 角色
我们在 com.jimmy.todoservice/service/RoleService.java
上添加实现的方法:
java
// 删除角色
void deleteRole(Long id);
然后在 com.jimmy.todoservice/service/impl/RoleServiceImpl.java
内添加 deleteRole
方法的实现:
java
@Override
public void deleteRole(Long id) {
Role role = roleRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("资源找不到"));
roleRepository.deleteById(id);
}
接着,我们添加相关的 controller
,在 com.jimmy.todoservice/controller/RoleController.java
上添加接口:
java
// 删除指定 id 的角色
@DeleteMapping("/{id}")
public ResponseEntity<String> deleteRole(@PathVariable("id") Long roleId) {
roleService.deleteRole(roleId);
return ResponseEntity.ok("删除角色成功!");
}
启动项目,在 Postman
上测试接口:
bash
[DELETE] http://localhost:8080/api/roles/1
注意:前提要登陆,带上 token 请求上面的接口
进入 Navicat
查看相关的记录已经被删除。
如果读者理解了角色的增删改查,那么 TODO 的增删改查大同小异
角色和用户关联
下面,我们将角色和用户进行关联。
建立表关联
用户表建立外键,外键为角色表的 ID
:
更改 com.jimmy.todoservice/entity/User.java
的文件:
java
package com.jimmy.todoservice.entity;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
// User 关联 Role, 多对多关联
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "users_roles", // 关联表
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), // 指定关联到当前实体的外键列
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id") // 指定到关联的另一个实体的外键列(哪个实体呢?可以通过下面的 Set<Role> 中推断出是 roles 实体)
)
private Set<Role> roles;
}
我们重新启动项目之后,可以通过 Navicat
查看到数据库中多出了一个表 users_roles
。
实操
我们来操作下,给用户添加一个角色关联。
在文件 com.jimmy.todoservice/service/UserService.java
中,添加内容:
java
// 用户关联角色
void assignRoleToUser(Long userId, Set<Long> roleId);
然后在文件 com.jimmy.todoservice/service/impl/UserServiceImpl.java
中进行实现,添加下面的代码:
java
@Override
public void assignRoleToUser(Long userId, Set<Long> roleIds) {
User user = userRepository.findById(userId).orElseThrow(() -> new ResourceNotFoundException("用户找不到"));
Set<Role> roles = new HashSet<>(roleRepository.findAllById(roleIds));
user.setRoles(roles);
userRepository.save(user);
}
最后,添加对应 controller
,在文件 com.jimmy.todoservice/controller/UserController.java
中添加对应的接口,如下:
java
// 给用户设定角色
@PutMapping("/{id}")
public ResponseEntity<String> assignRoleToUser(@PathVariable("id") Long userId, @RequestBody Set<Long> roleIds) {
userService.assignRoleToUser(userId, roleIds);
return new ResponseEntity<>("成功返回", HttpStatus.OK);
}
假设角色表中我们已经有了数据
ID
为2
和3
,用户表中有数据ID
为8
。
我们通过 Postman
发起请求:
bash
[PUT] http://localhost:8080/api/users/8
[BODY] [2, 3]
注意,我们得添加登陆的 token 凭证
执行成功后,我们可以通过 Navacat
中,数据库的中间表 users_roles
中写入数据。
整合角色权限到 security
将角色的权限整合到 Security
中。
前言
在之前的章节中,我们在 com.jimmy.todoservice/security/CustomUserDetailsService.java
中模拟,写死了角色。如下:
java
// Set<GrantedAuthority> authorities = null; // null 会报错,不允许空
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER")); // 模拟,写死
那么,本小节讲解,如何应用用户关联的角色到鉴权中。
获取角色的 roles
我们改写前言中的代码内容如下:
java
// Set<GrantedAuthority> authorities = null; // null 会报错,不允许空
// List<GrantedAuthority> authorities = new ArrayList<>();
// authorities.add(new SimpleGrantedAuthority("ROLE_USER")); // 模拟,写死
Set<GrantedAuthority> authorities = user.getRoles().stream()
.map((role) -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toSet());
设定接口的角色权限
我们更改 com.jimmy.todoservice/config/SpringSecurityConfig.java
的文件内容如下:
java
package com.jimmy.todoservice.config;
@Configuration
@EnableMethodSecurity
@AllArgsConstructor
public class SpringSecurityConfig {
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf((csrf) -> csrf.disable())
.authorizeHttpRequests((authorize) -> {
authorize.requestMatchers(HttpMethod.GET, "/api/roles/**").hasAnyAuthority("ADMIN", "USER"); // + 添加,[GET] 接口只有角色 ADMIN 或者 USER 有权限访问
authorize.requestMatchers(HttpMethod.PUT, "/api/roles/**").hasAuthority("ADMIN"); // + 添加,[PUT] 接口只有角色是 ADMIN 有权限访问
authorize.requestMatchers("/api/auth/**").permitAll();
authorize.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll();
authorize.anyRequest().authenticated();
}).httpBasic(Customizer.withDefaults());
// 认证, jwt
httpSecurity.exceptionHandling(exception -> exception
.authenticationEntryPoint(jwtAuthenticationEntryPoint));
httpSecurity.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
验证
我们添加带角色为 USER
的用户,用起 token
凭证发起请求访问接口
bash
[PUT] http://localhost:8080/api/roles/2
{
"description": "管理员"
}
访问不了,报错 401
。
当用其 token
访问接口
bash
[GET] http://localhost:8080/api/roles
则能成功获取到角色列表。
接下来就是实现 TODO
的 CURD
了,请参考小节角色表增删改查。
项目部署到服务器 - 练手
我们先拿一个 Demo
的项目来部署,并没有数据库。GET
接口 /api/demo
只是返回一个字符串的信息 Hello world!
。
我们将参考到的文章 - java jar 包发布
假设我们已经打包好了文件 **.jar
,比如 todo-service-0.0.1-SNAPSHOT.jar
。
服务器登陆
假设我们知道了服务器的地址,账号(用户名),密码,我们可以通过下面的命令行进行登陆:
bash
1. ssh 账号@1*.1**.8*.1**
2. 根据提示输入密码
成功登陆。
安装 java
因为我们是 java
服务,所以我们安装相关的包。假设这里的服务器中可用 yum
管理包。
bash
sudo yum update // 升级
sudo yum install java-17-openjdk // 安装相关的 java 的版本
安装 nginx 并将配置
同理,安装 nginx
。
安装成功后,一般情况下,我们通过 whereis nginx
就可以查看到 config
的配置文件路径。如果我们找不到的话,我们可以通过 sudo nginx -t
查看其所在位置,比如:
bash
> sudo nginx -t
nginx: the configuration file /opt/homebrew/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /opt/homebrew/etc/nginx/nginx.conf test is successful
由此可以知道 config
文件在 /opt/homebrew/etc/nginx/
文件夹下。
好,那么我们来配置下服务。
进入相关的 **.config
之后,我们添加下面的内容:
bash
# service - from -jimmy
upstream api {
server 127.0.0.1:6000;
keepalive 2000;
}
server {
# 其他内容忽略
# service - from -jimmy
location /api {
proxy_pass http://api;
}
}
保存后,我们可以通过 sudo nginx -t
来查看配置语法是否正确。
上传 jar 包,并启动
我们通过下面的命令行上传 jar
包:
bash
scp -r /path/to/target/demo-0.0.1-SNAPSHOT.jar 账号@1*.1**.8*.1**:/usr/local/nginx/ // 同步本地信息到远程服务器
这里我们把 jar
包放在了文件夹 /usr/local/nginx/
下。
那么,我们来启动该服务,并指定运行的端口是 6000
,这个要和上面配置的 config
中 upstream api
配置的 server
的端口有关。
进入 /usr/local/nginx/
后,我们可以通过方法一,如下:
bash
java -jar todo-service-0.0.1-SNAPSHOT.jar --server.port=6000
来启动服务,这个方式,在关闭掉控制台后,服务会中断。
方法一,局限。那么,我们使用方法二 nohup java -jar -Dserver.port=6000 todo-service-0.0.1-SNAPSHOT.jar > output-demo-0.0.1-SNAPST.txt &
,服务常驻内存,关闭控制台也不会影响。
此时,通过访问 [GET] https://domain.com/api/demo
接口,则正确返回字符串数据。
那么,方法二,如果我们要关闭服务怎么办?
我们可以使用下面的方法:
bash
# 1. 使用 ps 命令行查找 java 进程的 PID
ps aux | grep java
# 使用 kill 命令行终止 java 进程,假设 java 进程 ID 是 12345
kill -9 12345
# -9 参数表示强制终止进程
【完✅】
谢谢你看到这里🌹