Spring Security là một dự án nổi bật trong hệ sinh thái Spring. Spring Security cung cấp các dịch vụ bảo mật toàn diện cho các ứng dụng doanh nghiệp có nền tảng Java EE.
Để sử dụng Spring Security, ta cần thêm Dependency vào pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>Spring Security cung cấp 2 cơ chế cơ bản:
Authentication(xác thực): là tiến trình thiết lập mộtprincipal.Principalcó thể hiểu là một người, hoặc một thiết bị, hoặc một hệ thống nào đó có thể thực hiện một hành động trong ứng dụng của bạn.Authorization(phân quyền): là tiến trình quyết định xem một principal có được phép thực hiện một hành động trong ứng dụng của bạn hay không. Trước khi diễn tiến tớiAuthorization,principalcần phải được thiết lập bởiAuthentication.
SecurityContextlà interface cốt lõi của Spring Security, lưu trữ tất cả các chi tiết liên quan đến bảo mật trong ứng dụng. Khi chúng ta kích hoạt Spring Security trong ứng dụng thìSecurityContextcũng sẽ được kích hoạt theo.- Chúng ta sẽ không truy cập trực tiếp vào
SecurityContext, thay vào đó sẽ sử dụng lớpSecurityContextHolder. Lớp này lưu trữSecurityContexthiện tại của ứng dụng, bao gồm chi tiết củaprincipalđang tương tác với ứng dụng. Spring Security sẽ dùng một đối tượngAuthenticationđể biểu diễn thông tin này - Đoạn code dưới đây sẽ giúp chúng ta lấy được
usernamecủaprincipalđã được xác thực
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails) principal).getUsername();
} else {
String username = principal.toString();
}UserDetails là một interface cốt lõi của Spring Security. Nó đại diện cho một principal nhưng theo một cách mở rộng và cụ thể hơn. UserDetails bao gồm các method sau:
getAuthorities(): trả về danh sách các quyền của người dùnggetPassword(): trả về password đã dùng trong qúa trình xác thựcgetUsername(): trả về username đã dùng trong qúa trình xác thựcisAccountNonExpired(): trả về true nếu tài khoản của người dùng chưa hết hạnisAccountNonLocked(): trả về true nếu người dùng chưa bị khóaisCredentialsNonExpired(): trả về true nếu chứng thực (mật khẩu) của người dùng chưa hết hạnisEnabled(): trả về true nếu người dùng đã được kích hoạtUserDetailsmới chỉ cung cấp các phương thức để truy cập các thông tin cơ bản của người dùng. Để mở rộng thêm các thông tin, chúng ta sẽ tạo một lớpCustomUserDetailsimplementsorg.springframework.security.userdetails.UserDetails
UserDetailsService là một interface có duy nhất một phương thức
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
Tham số truyền vào chỉ gồm có username của người dùng. Ta sẽ tìm kiếm trong CSDL, record thỏa mãn username. Nếu không tìm thấy, ta sẽ ném ra ngoại lệ UsernameNotFoundException.
Phương thức loadUserByUsername() sẽ trả về một implementation của UserDetails.
Ở phần trên đã đề cập đến phương thức getAuthorities(). Phương thức này sẽ trả về một tập hợp các đối tượng GrantedAuthority. Một GrantedAuthority là một quyền được ban cho principal.
Tiếp tục sử dụng các Class, Table User, Role ở phần Entity, ORM, Relationship, Fetch Type (Lazy vs Eager)
user
create table "user"
(
id serial not null
constraint user_pk
primary key,
username varchar(50) not null,
password varchar(255),
email varchar(255) not null,
role_id integer default 3
constraint user_role_id_fk
references role
);role
create table role
(
id integer generated by default as identity
constraint role_pkey
primary key,
name varchar(255)
);User.java
Sử dụng lombok để code trở nên ngắn gọn hơn.
@Entity
@Table(name = "user", schema = "public")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String email;
@ManyToOne
@JoinColumn(name = "role_id", nullable = false)
private Role role;
}Role.java
@Entity
@Table(name = "role", schema = "public")
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "role")
private Set<User> userList = new HashSet<>();
public int getId() {
return id;
}
public String getName() {
return name;
}
}userRepository.java
Bằng việc kế thừa JpaRepository, ta có thể dễ dàng truy xuất tới DB mà không cần phải sử dụng Native Query. Điều này sẽ được tìm hiểu kỹ hơn ở phần sau.
@Repository("userRepository")
public interface userRepository extends JpaRepository<User, Integer> {
Optional<User> findUserByUsername(String Username);
}userDetailServiceImpl.java
@Service("userDetailServiceImpl")
@RequiredArgsConstructor
public class userDetailServiceImpl implements UserDetailsService {
private final userRepository userRepository;
@Override
public UserDetails loadUserByUsername(String Username) throws UsernameNotFoundException {
Optional<User> user = userRepository.findUserByUsername(Username);
if (user == null) {
System.out.println("User not found!" + Username);
throw new UsernameNotFoundException("User " + Username + " was not found in the database");
}
System.out.println("Found user!" + Username);
UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.get().getUsername(), user.get().getPassword(), getRole(user));
System.out.println(userDetails);
return userDetails;
}
public Set<SimpleGrantedAuthority> getRole(Optional<User> user) {
Role role = user.get().getRole();
return Collections.singleton(new SimpleGrantedAuthority(role.getName()));
}
}AppConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class AppConfig extends WebSecurityConfigurerAdapter{
@Bean
public NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// Được quyền truy cập khi chưa login
http.authorizeRequests().antMatchers("/login", "/signup", "/").permitAll();
// Có những quyền Admin, Mod, Member sẽ được truy cập
http.authorizeRequests().antMatchers("/user/*").hasAnyAuthority("Admin", "Mod", "Member");
//chỉ có quyền Admin, Mod mới được truy cập
http.authorizeRequests().antMatchers("/admin").hasAnyAuthority("Admin", "Mod");
// Khi không đủ quyền truy cập sẽ bị chuyển hướng
http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/error");
// Custom login
http.authorizeRequests().and().formLogin()
.loginProcessingUrl("/authentication")
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error=true")
.usernameParameter("username")
.passwordParameter("password")
.and().logout().logoutUrl("/logout");
}
}Controller.java
@org.springframework.stereotype.Controller
@RequiredArgsConstructor
public class Controller {
private final userRepository userRepository;
@GetMapping("/user")
public String userPage(Model model, Principal principal) {
User personal = userRepository.findUserByUsername(principal.getName()).get();
model.addAttribute("personal", personal);
return "user";
}
@GetMapping("/user/{name}")
public String userPage(@PathVariable("name") String name, Model model) {
User personal = userRepository.findUserByUsername(name).get();
model.addAttribute("personal", personal);
return "user";
}
@GetMapping("/login")
public String loginPage() {
return "login";
}
@GetMapping("/signup")
public String signUpPage() {
return "signup";
}
@GetMapping("/admin")
public String adminPage() {
return "admin";
}
@GetMapping("/error")
public String errorPage() {
return "error";
}
}login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Sign In</title>
<link rel="stylesheet" th:href="@{/css/css.css}">
</head>
<body>
<div class="wrapper fadeInDown">
<div id="formContent">
<!-- Tabs Titles -->
<h2 class="inactive underlineHover"><a href="/">HOME</a></h2>
<h2 class="active"> Sign In </h2>
<h2 class="inactive underlineHover"><a href="/signup">SIGN UP</a></h2>
<!-- /login?error=true -->
<div th:if="${#request.getParameter('error') == 'true'}"
style="color:red;margin:10px 0px;">
Login Failed!!!
</div>
<!-- Login Form -->
<form th:action="@{/authentication}" method="post">
<input type="text" id="username" name="username" class="fadeIn first" placeholder="Username">
<input type="password" id="password" name="password" class="fadeIn second" placeholder="Password">
<input type="submit" class="fadeIn third" value="Log In">
</form>
<!-- Remind Passowrd -->
<div id="formFooter">
<a class="underlineHover" href="#">Forgot Password?</a>
</div>
</div>
</div>
</body>
</html>signup.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Sign Up</title>
<link rel="stylesheet" th:href="@{/css/css.css}">
</head>
<body>
<div class="wrapper fadeInDown">
<div id="formContent">
<!-- Tabs Titles -->
<h2 class="inactive underlineHover"><a href="/">HOME</a></h2>
<h2 class="inactive underlineHover"><a href="/login">SIGN IN</a></h2>
<h2 class="active"> Sign Up </h2>
<!-- /login?error=true -->
<div th:if="${#request.getParameter('error') == 'true'}"
style="color:red;margin:10px 0px;">
Login Failed!!!
</div>
<!-- Login Form -->
<form th:action="@{/authentication}" method="post">
<input type="text" id="username" name="username" class="fadeIn first" placeholder="Username">
<input type="password" id="password" name="password" class="fadeIn second" placeholder="Password">
<input type="password" id="repassword" name="repassword" class="fadeIn second" placeholder="RePassword">
<input type="submit" class="fadeIn third" value="Sign Up">
</form>
<!-- Remind Passowrd -->
<div id="formFooter">
</div>
</div>
</div>
</body>
</html>user.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${personal.getUsername()}">User Info</title>
<link rel="stylesheet" th:href="@{/css/css.css}">
</head>
<body>
<div class="wrapper fadeInDown">
<div id="formContent">
<!-- Tabs Titles -->
<h2 class="inactive underlineHover"><a href="/">HOME</a></h2>
<h2 class="active">User Info</h2>
<h2 class="inactive underlineHover"><a href="/admin">Admin</a></h2>
<h2 class="inactive underlineHover"><a href="/logout">Log Out</a></h2>
<th:block th:object="${personal}">
<div class="list">
<b th:utext="${personal.getUsername()}"></b>
</div>
<div class="list1">
<b>User: </b>
<span th:utext="${personal.getUsername()}"></span>
</div>
<div class="list1">
<b>Role: </b>
<span th:utext="${personal.getRole().getName()}"></span>
</div>
<div class="list1">
<b>Mail: </b>
<span th:utext="${personal.getEmail()}"></span>
</div>
</th:block>
<div id="formFooter">
Hi, <span th:text="${#request.userPrincipal.getName()}"></span>!
</div>
</div>
</div>
</body>
</html>admin.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:title="${#request.userPrincipal.name}">User Info</title>
<link rel="stylesheet" th:href="@{/css/css.css}">
</head>
<body>
<div class="wrapper fadeInDown">
<div id="formContent">
<!-- Tabs Titles -->
<h2 class="inactive underlineHover"><a href="/">HOME</a></h2>
<h2 class="inactive underlineHover"><a href="/user">User Info</a></h2>
<h2 class="active"> Admin</h2>
<h2 class="inactive underlineHover"><a href="/logout">Log Out</a></h2>
<h1 style="color: red">Page only for Manager!!</h1>
<div id="formFooter">
Hi, <span th:text="${#request.userPrincipal.getName()}"></span>!
</div>
</div>
</div>
</body>
</html>error.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Error</title>
<link rel="stylesheet" th:href="@{/css/css.css}">
</head>
<body>
<div class="wrapper fadeInDown">
<div id="formContent">
<!-- Tabs Titles -->
<h2 class="inactive underlineHover"><a href="/">HOME</a></h2>
<h2 class="inactive underlineHover" th:if="${#request.userPrincipal == null}"><a href="/login">Sign
In</a></h2>
<h2 class="inactive underlineHover" th:if="${#request.userPrincipal == null}"><a href="/signup">Sign
Up</a></h2>
<h2 class="inactive underlineHover" th:if="${#request.userPrincipal != null}"><a href="/user">User</a></h2>
<h2 class="inactive underlineHover" th:if="${#request.userPrincipal != null}"><a href="/logout">Log
Out</a></h2>
<h1 style="color: red">You cannot access this page!!</h1>
<div id="formFooter">
Hi, <span th:text="${#request.userPrincipal.getName()}" th:if="${#request.userPrincipal != null}"></span>
<span th:if="${#request.userPrincipal == null}">Guest</span> !
</div>
</div>
</div>
</body>
</html>- JSON Web Token (JWT) là 1 tiêu chuẩn mở (RFC 7519) định nghĩa cách thức truyền tin an toàn giữa các thành viên bằng 1 đối tượng JSON.
- Thông tin này có thể được xác thực và đánh dấu tin cậy nhờ vào “chữ ký” của nó.
- Phần chữ ký của JWT sẽ được mã hóa lại bằng
HMAChoặcRSA.
eyJhbGciOiJIUzUxMiJ9.
eyJzdWIiOiJUZXN0ZXIyIiwiZXhwIjoxNjIxNDMwNTc5LCJpYXQiOjE2MjE0MTI1Nzl9.
p4u7eWth4RocvgFrN_E0TZ6gWOZA_2qEv1DV3gC6KloFJekUP53ZB4Lk324g2UdF3xbMcxv5xIbKq0i0z3oUlA
JWT trên gồm 3 phần được phân tách bằng dấu .. Cấu trúc của nó như sau:
<base64-encoded header>.<base64-encoded payload>.<HMACSHA512(base64-encoded signature)> Header: {"alg":"HS512"} - alg: thuật toán dùng để mã hoá (HMAC SHA hoặc RSA)
Payload:
{
"sub": "Tester2",
"exp": 1621430579,
"iat": 1621412579
}-
Đây là nơi chứa nội dung của thông tin
-
Tuỳ vào ứng dụng mà chúng ta sẽ ràng buộc với thông tin cần thiết. Một số trường trong Payload là:
- iss (issuer): tổ chức phát hành token (không bắt buộc)
- sub (subject): chủ đề của token (không bắt buộc)
- aud (audience): đối tượng sử dụng token (không bắt buộc)
- exp (expired time): thời điểm token sẽ hết hạn (không bắt buộc)
- nbf (not before time): token sẽ chưa hợp lệ trước thời điểm này
- iat (issued at): thời điểm token được phát hành, tính theo UNIX time
- jti: JWT ID
Signature: phần chữ ký được tạo bằng kết hợp 2 phần Header và Payload, sau đó được mã hoá bằng thuật toán đã khai báo ở phần Header thông qua một khoá.
- User thực hiện login bằng cách gửi id/password hay sử dụng các tài khoản mạng xã hội lên phía Authentication Server (Server xác thực)
- Authentication Server tiếp nhận các dữ liệu mà User gửi lên để phục vụ cho việc xác thực người dùng. Trong trường hợp thành công, Authentication Server sẽ tạo một JWT và trả về cho người dùng thông qua response.
- Người dùng nhận được JWT do Authentication Server vừa mới trả về làm "chìa khóa" để thực hiện các "lệnh" tiếp theo đối với Application Server.
- Application Server trước khi thực hiện yêu cầu được gọi từ phía User, sẽ verify JWT gửi lên. Nếu OK, tiếp tục thực hiện yêu cầu được gọi.
Tiếp tục sử dụng ví dụ ở trên.
JwtRequest.java: nhận User / Pass từ Client
public class JwtRequest {
private String username;
private String password;
public JwtRequest() {
}
public JwtRequest(String username, String password) {
this.username = username;
this.password = password;
}
}
//getter / setterJwtUtils.java: Có các chức năng như Generate Token, Validate token,...
@Component
public class jwtUtils implements Serializable {
private final String jwtSecret = "thanhtpham";
private final long jwtExp = 5 * 1000 * 3600;
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claimsResolver.apply(claims);
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + jwtExp))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}AuthTokenFilter.java: tạo một Filter extends OncePerRequestFilter. KHi có bất cứ request nào tới thì Filter này sẽ được thực thi và kiểm tra xem token có hợp lệ hay không. Nếu hợp lệ thì nó sẽ set Authentication trong Security Context để chỉ định rằng User đã được xác thực.
@Component
@RequiredArgsConstructor
public class AuthTokenFilter extends OncePerRequestFilter {
private final jwtUtils jwtUtils;
private final userDetailServiceImpl userDetailServiceImpl;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtUtils.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
System.out.println("JWT Token has expired");
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailServiceImpl.loadUserByUsername(username);
if (jwtUtils.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}AppConfig.java Đầu tiên sẽ inject Filter vào
private final AuthTokenFilter authTokenFilter;Tiếp theo, ta sẽ Override authenticationManagerBean trong WebSecurityConfigurerAdapter để inject AuthenticationManager.
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}Đoạn code dưới đây vào configure
http.authorizeRequests().antMatchers("/login", "/signup", "/", "/api/login").permitAll();
http.authorizeRequests().antMatchers(HttpMethod.GET, "/api/get/**").authenticated();
http.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);RestfulAPI.java
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/api")
public class RestAPI {
private final userRepository userRepository;
private final userDetailServiceImpl userDetailServiceImpl;
private final jwtUtils jwtUtils;
private final AuthenticationManager authenticationManager;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody JwtRequest authenticationRequest) throws Exception {
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
System.out.println("ok");
final UserDetails userDetails = userDetailServiceImpl
.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtUtils.generateToken(userDetails);
return ResponseEntity.ok(token);
}
private void authenticate(String username, String password) throws Exception {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
System.out.println(new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
}
@GetMapping(value = "/get")
public List<User> getAllUser() {
return userRepository.findAll();
}
@GetMapping(value = "/get/{id}")
public User getUser(@PathVariable int id) {
return userRepository.findById(id).get();
}
}https://www.marcobehler.com/guides/spring-security
https://techmaster.vn/posts/36295/spring-security-ban-sau-ve-authentication-va-authorization-p1