Spring Security Custom Login (I)

在剛剛我們說明了如何使用最簡易的登入,但是這些密碼都是明碼,更何況並沒有使用DB來做存取,也代表是沒辦法動態新增使用者的,因此我們需要讓Spring也使用我們自己的UserTable。

Entity

既然都要使用自己的table進行登入,免不了就是要設定一些東西,如dao類的。

User

@Entity
@Table(name="WEB_USERS")
public class User {

    @Id
    @Column(name="USER_ID")
    private String userId;

    @Column(name="USER_NAME")
    private String userName;

    @Column(name="PASSWORD")
    private String password;

    @OneToMany(fetch=FetchType.EAGER)
    @JoinColumn(name="USER_ID")
    private Set<UserRole> userRoles;

    public Set<UserRole> getUserRoles() {
        return userRoles;
    }

    public void setUserRoles(Set<UserRole> userRoles) {
        this.userRoles = userRoles;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User [userId=" + userId + ", userName=" + userName + ", password=" + password
                + ", userRoles=" + userRoles + "]";
    }

}

UserRole

@Entity
@Table(name="WEB_USER_ROLES")
public class UserRole {

    @Id
    @Column(name="ROLE_ID")
    private String roleId;

    @Column(name="ROLE")
    private String role;

    @Column(name="USER_ID")
    private String userId;

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    public String getRoleId() {
        return roleId;
    }

    public void setRoleId(String roleId) {
        this.roleId = roleId;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    @Override
    public String toString() {
        return "UserRole [roleId=" + roleId + ", role=" + role + "]";
    }    

}

Dao

UserDao

@Repository
public interface UserDao extends JpaRepository<User, String>{

    @Query("select u from User u where u.userName=?1")
    User getUserByUserName(String userName);

}

UserRoleDao

@Repository
public interface UserRoleDao extends JpaRepository<UserRole, String>{

}

Security Config

那麼這邊我們需要新增以下幾種

  1. DataSource 不想解釋
  2. PasswordEncoder 密碼加密方式,這邊我使用BCryptPasswordEncoder,內部實現方式為Hash + Salt,但我覺得可以研究一下使用自定義的加密算法,每個版本加密算法所產生的編碼可能都不大相同,如果使用官方的算法,很有可能被綁住。
  3. AuthenticationProvider 原先的認證已經不適合我們,因此我們需要定義一個
  4. UserDetailsService 取得使用者的方式,我們需要重寫一個

PS. dataSource使用@Autowired方式取得,我設定在HibernateConfig裡面,在Spring with ORM Frameworks章節中可以看見。

package com.mvc.example.webconfig;

import javax.sql.DataSource;

import org.dom4j.swing.BranchTreeNode;
import org.hibernate.boot.registry.selector.SimpleStrategyRegistrationImpl;
import org.hibernate.loader.plan.exec.process.spi.ReturnReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;

import com.mvc.example.service.CustomUserDetailsService;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
        auth.userDetailsService(userDetailsService());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();

        daoAuthenticationProvider.setUserDetailsService(userDetailsService());
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());

        return daoAuthenticationProvider;
    }

    @Bean(name="userDetailsService")
    public UserDetailsService userDetailsService() {
        return new CustomUserDetailsService();
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        SessionRegistry sessionRegistry = new SessionRegistryImpl();
        return sessionRegistry;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/login").permitAll() //全部的人都可以嘗試登入
            .and()
                .formLogin() //這邊如果沒有使用下面的設定,Spring會自行產生簡易的登入頁面
                    .loginPage("/login") //登入頁面位置
                    .usernameParameter("username") //表單帳號參數名稱
                    .passwordParameter("password") //表單密碼參數名稱
                    .defaultSuccessUrl("/") //登入完成後導引至首頁
                    .permitAll() //此頁面沒有權限限制
            .and()
                .logout()
                    .logoutSuccessUrl("/login") //登出後跳轉至登入頁面
                    .invalidateHttpSession(true) //使該Session無效
            .and()
                .csrf() //使用csrf防禦機制
            .and()
                .sessionManagement() //管理Session  
                    .maximumSessions(1) //同時只能一個Session
                    .maxSessionsPreventsLogin(true) //只能讓一個帳號存在一個Session
                    .sessionRegistry(sessionRegistry()) //監聽Session的欄位,基本上預設即可,若要做更多用途可以自行撰寫
                    .and()
            .and()
                .exceptionHandling().accessDeniedPage("/403"); //將所有403頁面導向這裡 
    }
}

CustomUserDetailsService

剛剛我們已經說要重寫這個類別,主因是我們需要提供認證方式,那再讓我們看看如何使用

public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserDao userDao;

    //登入時會使用這個方法取得資料,傳入參數為帳號,若確認有此帳號,會建立一個User物件,後續驗證會使用。
    @Override
    public UserDetails loadUserByUsername(String arg0) throws UsernameNotFoundException {

        User user = userDao.getUserByUserName(arg0);

        if (user == null) {
            throw new UsernameNotFoundException("user not found");
        }

        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for(UserRole role : user.getUserRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getRole()));
        }

        return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getPassword(), authorities);
    }

}

results matching ""

    No results matching ""