Khi lập trình web trên Spring MVC để phân quyền truy cập cho websites đơn giản chúng ta sử dụng:

  1. session trực tiếp trong controller trong từng request.
  2. sử dụng filter để đơn giản hoá việc áp dụng session.
  3. chuyên nghiệp hơn thì sử dụng thư viện Spring Security.

Trong bài viết này sẽ đề cập đến cách thứ 2 nhưng áp dụng thêm kiến thức về Java Annotation để việc sử dụng chuyên nghiệp như Spring Security (trên khía cạnh tối giản code khi triển khai trên từng request).

Với cách làm này hoàn toàn có thể nâng cấp thành một thư viện áp dụng cho nhiều dự án khác nhau.

Mở đầu

Việc sử dụng filter thường có 2 hướng (mô tả cho servlet):

  1. khai báo role và url tương ứng dưới xml (web.xml):
    <filter>
       <filter-name>Admin</filter-name>
       <filter-class>AuthenFilter</filter-class>
       <init-param>
          <param-name>role</param-name>
          <param-value>admin</param-value>
       </init-param>
    </filter>
    
    <filter-mapping>
       <filter-name>Admin</filter-name>
       <url-pattern>/admin</url-pattern>
    </filter-mapping>
    
    <filter>
       <filter-name>Emp</filter-name>
       <filter-class>AuthenFilter</filter-class>
       <init-param>
          <param-name>role</param-name>
          <param-value>admin</param-value>
       </init-param>
    </filter>
    
    <filter-mapping>
       <filter-name>Emp</filter-name>
       <url-pattern>/employee</url-pattern>
    </filter-mapping>
    Sau đó trong AuthenFilter chỉ cần get param role và tiến hành so sánh với user hiện tại. Vấn đề là chúng ta phải lặp đi lặp lại khai báo như đoạn code trên.
     
  2. Khai báo một lần filter cho tất cả các url:
    <filter>
       <filter-name>AuthenFilter</filter-name>
       <filter-class>AuthenFilter</filter-class>
    </filter>
    
    <filter-mapping>
       <filter-name>AuthenFilter</filter-name>
       <url-pattern>/*</url-pattern>
    </filter-mapping>
    Nhưng trong AuthenFilter chúng ta phải so sánh lại cả url lẫn role của user lặp đi lặp lại trong code java.
     

Dễ nhận thấy 2 cách trên thường mất nhiều thời gian và viết đi viết lại những đoạn khai báo, so sánh nhàm chán. Và khó sử dụng lại trong các dự án khác nhau. Vậy nên với giải pháp sẽ trình bày dưới đây sẽ phần nào hạn chế được những nhược điểm trên.

 

 

Mô tả

  1. Chúng ta sẽ dùng filter như cách thứ 2 ở trên để bắt toàn bộ các request đưa vào một filter duy nhất. 
  2. Đối với Spring sử dụng Interceptor thay thế cho filter như trong servlet (Interceptor có chức năng tương đương filter).
  3. Khai báo annotation interface, sử dụng anotation tại controller và tiến hành reflect runtime tại Interceptor.

 

Demo

Code demo chạy trên maven, trong bài này sẽ bỏ qua các khởi tạo và cấu hình cơ bản của maven và spring mvc.

  1. Cấu trúc thư mục:
  2. Cấu hình ContextLoaderListener trong web.xml:
    <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
  3. Khởi tạo annotation interface (Auth.java):
    @Retention(RetentionPolicy.RUNTIME)
    // khai báo annotation này có hiệu lực với method
    @Target(ElementType.METHOD)
    public @interface Auth {
        // ở đây mình set cứng gồm 1 số quyền sau, chúng ta có thể thay thế sang
        // String hoặc int để sử dụng kèm db
        public enum Role {
            LOGIN, ADMIN, EMPLOYEE
        };
    
        // khai báo properties cho anotation với giá trị mặc định là LOGIN
        // ví dụ: @Auth(role = Role.LOGIN)
        public Role role() default Role.LOGIN; // default = @Auth()
    }
    
  4. Khởi tạo Interceptor (AuthInterceptor.java):
    public class AuthInterceptor implements HandlerInterceptor {
    
        // preHandle sẽ được gọi khi có request (config request url ở dưới)
        @Override
        public boolean preHandle(HttpServletRequest request,
                HttpServletResponse response, Object handler) throws Exception {
    
            // trích xuất method tương ứng với request mapping trong controller
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
    
            // tìm trong method có khai báo anotation Auth?
            Auth roleAnnotation = AnnotationUtils
                    .findAnnotation(method, Auth.class);
            // nếu có lấy ra thuộc tính role, không trả về null
            Auth.Role role = roleAnnotation != null ? roleAnnotation.role() : null;
    
            // lấy các thông tin đăng nhập từ session
            HttpSession session = request.getSession();
            boolean isLogined = session.getAttribute("isLogin") != null ? (Boolean) session
                    .getAttribute("isLogin") : false;
            Auth.Role loginRole = session.getAttribute("role") != null ? (Auth.Role) session
                    .getAttribute("role") : null;
    
            // - trường hợp role yêu cầu của method = null bỏ qua interceptor này và
            // chạy bình thường
            // - khác null tức request này chỉ được thực hiên khi đã đăng nhập
            if (role != null) {
                // chưa đăng nhập chuyển hướng sang trang login để đăng nhập
                if (!isLogined) {
                    response.sendRedirect("login");
                    return false;
                } else {
                    // - trường hợp đã login tiến hành kiểm tra role
                    // - những trường hợp chỉ yêu cầu login mà không yêu cầu cụ thể
                    // role nào thì tất cả các role đều có quyền truy cập
                    // - trường hợp yêu cầu cụ thể loại role sau khi đăng nhập thì
                    // phải kiểm tra
                    // - không thoả mãn điều kiện dưới chuyển hướng sang trang
                    // denied
                    if (role != Auth.Role.LOGIN && role != loginRole) {
                        response.sendRedirect("deny?url=\""
                                + request.getRequestURL().toString() + "?"
                                + request.getQueryString() + "\"&role=" + role);
                        return false;
                    }
                }
            }
    
            return true;
        }
    }
    
  5. Cấu hình Interceptor trong spring web config (webapp-config.xml):
        <!-- Auth -->
        <mvc:interceptors>
            <mvc:interceptor>
                <mvc:mapping path="/**" />
                <mvc:exclude-mapping path="/login" />
                <beans:bean class="org.authinterceptor.AuthInterceptor" />
            </mvc:interceptor>
        </mvc:interceptors>
    <mvc:exclude-mapping path="/login" /> : bỏ qua trang login tránh trường hợp interceptor lặp vô hạn.
  6. Sử dụng trong controller (AuthControll.java):
    @Controller
    public class AuthController {
    
        @RequestMapping(value = "/", method = RequestMethod.GET)
        public String index() {
    
            return "index";
        }
    
        @RequestMapping(value = "/login", method = RequestMethod.GET)
        public String login(HttpSession session) {
            if (session.getAttribute("isLogin") != null
                    && (Boolean) session.getAttribute("isLogin") == true) {
                return "redirect:/success";
            }
            return "login";
        }
    
        @RequestMapping(value = "/login", method = RequestMethod.POST)
        public String checkLogin(@RequestParam("user") String user,
                @RequestParam("pass") String pass, HttpSession session) {
    
            if ("123456".equals(pass)) {
                if ("admin".equals(user)) {
                    session.setAttribute("isLogin", true);
                    session.setAttribute("user", user);
                    session.setAttribute("role", Auth.Role.ADMIN);
    
                    return "redirect:/success";
                } else if ("emp".equals(user)) {
                    session.setAttribute("isLogin", true);
                    session.setAttribute("user", user);
                    session.setAttribute("role", Auth.Role.EMPLOYEE);
    
                    return "redirect:/success";
                }
            }
    
            return "login";
        }
    
        @RequestMapping(value = "/deny", method = RequestMethod.GET)
        public String deny() {
    
            return "deny";
        }
    
        @Auth(role = Auth.Role.LOGIN)
        @RequestMapping(value = "/success", method = RequestMethod.GET)
        public String needLogin() {
            return "success";
        }
    
        @Auth(role = Auth.Role.LOGIN)
        @RequestMapping(value = "/logout", method = RequestMethod.GET)
        public String logout(HttpSession session) {
            session.removeAttribute("isLogin");
            session.removeAttribute("user");
            session.removeAttribute("role");
    
            return "redirect:/";
        }
    
        @Auth(role = Auth.Role.ADMIN)
        @RequestMapping(value = "/admin", method = RequestMethod.GET)
        public String admin() {
    
            return "admin";
        }
    
        @Auth(role = Auth.Role.EMPLOYEE)
        @RequestMapping(value = "/emp", method = RequestMethod.GET)
        public String employee() {
    
            return "emp";
        }
    }
    
  7. Screenshots:

Source-code

https://huythang38@bitbucket.org/huythang38/basicspringauthintercreptor.git


Leave a Comment

Fields with * are required.