Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

Spring Security, User service

1. Spring Security, User service

1.1. Giới thiệu chung

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ột principal. Principal có 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ới Authorization, principal cần phải được thiết lập bởi Authentication.

1.2 Các thành phần

Security Context, Authentication

  • SecurityContext là 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ì SecurityContext cũ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ớp SecurityContextHolder. Lớp này lưu trữ SecurityContext hiện tại của ứng dụng, bao gồm chi tiết của principal đang tương tác với ứng dụng. Spring Security sẽ dùng một đối tượng Authentication để biểu diễn thông tin này
  • Đoạn code dưới đây sẽ giúp chúng ta lấy được username của principal đã đượ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 và UserDetailsService

UserDetails

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ùng
  • getPassword(): trả về password đã dùng trong qúa trình xác thực
  • getUsername(): trả về username đã dùng trong qúa trình xác thực
  • isAccountNonExpired(): trả về true nếu tài khoản của người dùng chưa hết hạn
  • isAccountNonLocked(): trả về true nếu người dùng chưa bị khóa
  • isCredentialsNonExpired(): trả về true nếu chứng thực (mật khẩu) của người dùng chưa hết hạn
  • isEnabled(): trả về true nếu người dùng đã được kích hoạt UserDetails mớ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ớp CustomUserDetails implements org.springframework.security.userdetails.UserDetails
UserDetailsService

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.

GrantedAuthority

Ở 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.

1.3 Ví dụ

Tiếp tục sử dụng các Class, Table User, Role ở phần Entity, ORM, Relationship, Fetch Type (Lazy vs Eager)

Tạo Table trong DB

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)
  );

Mapping Class với DB

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;
        }

    }

Tạo Repository

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()));
        }
    }

Cấu hình

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";
    }

}

Front-end

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>

2. Spring Security với JWT

2.1. Khái niệm JWT

  • 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 HMAC hoặc RSA.

2.2. Cấu trúc

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 HeaderPayload, sau đó được mã hoá bằng thuật toán đã khai báo ở phần Header thông qua một khoá.

2.3. Nguyên lý hoạt động

  • 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.

2.4. Ví dụ

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 / setter

JwtUtils.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();
    }
}

Tham khảo

https://www.marcobehler.com/guides/spring-security

https://techmaster.vn/posts/36295/spring-security-ban-sau-ve-authentication-va-authorization-p1