基於springboot的security機制(自定義登陸頁面+基於內存身份認證+基於mybatis身份認證)

接着上一章節,咱們在這一章種討論如何在現有的ssm框架中加入security機制,說白了,就是爲咱們項目提供身份驗證的功能。現有的需求中大多項目都沒法脫離登陸註冊功能。若是開發時每一個模塊提供一個登陸註冊功能,整個項目就會臃腫不堪,單點登陸也就應用而生了。至於OAuth2與springBoot的結合咱們在隨後章節討論,這一章節討論security機制的簡單應用。

In-Memory Authentication

基於內存的身份認證功能。也就是說身份信息是保存到內存中。這種方式瞭解爲主,在實際開發中使用較少。html

1 搭建ssm+springsecurity框架

須要的依賴有mysql

- web(spring mvc),
- mybatis(mybatis數據庫),
- mysql(mysql數據庫驅動),
- security(安全校驗機制)
> spring init -g=com.briup.apps -a=app04 -p=war -d=web,mybatis,mysql,security app04
   > cd app04
   > mvn install

構建項目過程當中依舊會報沒有指定驅動類的異常,解決方案仍是按照上一章節的方式,在application.properties中進行配置,而後在pom.xml中配置熱部署的依賴(方便開發)git

配置就緒後啓動項目github

> mvn spring-boot:run

clipboard.png

哈,是否是有些意外,咱們就沒作什麼事情,居然具備受權的功能了,這是security默認幫咱們實現的功能,那麼用戶名密碼是什麼呢? 用戶名默認爲user,密碼在啓動項目的時候會打印到控制檯。web

clipboard.png

若是咱們直接點擊取消,提示未受權異常。spring

clipboard.png

刷新頁面後進行登陸。輸入user/console中密碼,出現以下錯誤,不過這個錯誤咱們是能理解的,404找不到,說明沒有配置服務。sql

clipboard.png

2 自定義受權

在默認受權管理中若是咱們想添加用戶改怎麼辦?若是咱們想自定義登陸頁面怎麼辦?若是咱們想自定義攔截怎麼辦?數據庫

2.1 添加自定義用戶

實際上咱們項目之全部具備受權功能,是security框架幫咱們實現的。也就是WebSecurityConfigurerAdapter這個適配器完成,若是想要改變其默認行爲,那能夠重寫該適配器中的一些方法。安全

/**
 * 自定義身份驗證類(用於重寫WebSecurityConfigurerAdapter默認配置)
 * @Configuration     表示這是一個配置類
 * @EnableWebSecurity    容許security
 * configure()     該方法重寫了父類的方法,用於添加用戶與角色
 * */
@Configuration
@EnableWebSecurity
public class AuthConfig extends WebSecurityConfigurerAdapter {
    
    /**
     * 重寫該方法,添加自定義用戶
     * */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
        .withUser("admin").password("admin").roles("ADMIN")
        .and()
        .withUser("terry").password("terry").roles("USER")
        .and()
        .withUser("larry").password("larry").roles("USER");
    }
}

重啓服務進行測試session

clipboard.png

當用戶名密碼輸入錯誤的時候,出現如下界面

clipboard.png

當用戶名密碼輸入正確的時候,是能夠繼續訪問服務,因爲咱們還麼有提供任何服務,全部均會出現404異常。

2.2 提供服務

訂單控制器 OrderController

@RestController
@RequestMapping("/orders")
public class OrderController {
    
    @GetMapping("/findAll")
    public String findAll() {
        return "findAll";
    }

}

用戶管理控制器 UserController

@RestController
@RequestMapping("/users")
public class UserController {
    
    @GetMapping("/findAll")
    public String findAll() {
        return "user list";
    }
}

緊接着在AuthConfig 中配置權限。

@Configuration
@EnableWebSecurity
public class AuthConfig extends WebSecurityConfigurerAdapter {
    
    /**
     * 重寫該方法,設定用戶訪問權限
     * 用戶身份能夠訪問 訂單相關API
     * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        .antMatchers("/orders/**").hasRole("USER")    //用戶權限
        .antMatchers("/users/**").hasRole("ADMIN")    //管理員權限
        .antMatchers("/login").permitAll()
        .and()
        .formLogin();
        
        //super.configure(http);
    }

    /**
     * 重寫該方法,添加自定義用戶
     * */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
        .withUser("admin").password("admin").roles("ADMIN","USER")
        .and()
        .withUser("terry").password("terry").roles("USER")
        .and()
        .withUser("larry").password("larry").roles("USER");
    }
    
}

重啓服務進行登陸

  • 若是使用admin帳號登陸則users相關API和orders相關API均可以訪問
  • 若是使用terry帳號登陸則只能訪問orders相關API

clipboard.png

3 自定義登陸頁面

默認狀況下,當用戶沒有登陸就去訪問受保護資源時,系統會默認請求/login(get方式),這時重定向到登陸頁(spring security自帶)。當輸入用戶名密碼點擊登陸按鈕的時候,系統會請求/login(post方式)。如今咱們但願自定義登陸頁面(默認的登陸頁面很醜),可是身份校驗仍是但願由security來進行。這時候咱們只須要將登陸頁面重定向到咱們自定義頁面便可,這時候DIY表單,可是在這裏切記一點。登陸頁面重定向的地址和表單提交的地址務必一致!

  • 自定義配置 AuthConfig

在原來的基礎上擴展了DIY登陸頁面的控制器的設置

/**
 * 自定義身份驗證類(用於重寫WebSecurityConfigurerAdapter默認配置)
 * @Configuration     表示這是一個配置類
 * @EnableWebSecurity    容許security
 * configure()     該方法重寫了父類的方法,用於添加用戶與角色
 * */
@Configuration
@EnableWebSecurity
public class AuthConfig extends WebSecurityConfigurerAdapter {
    
    /**
     * 重寫該方法,設定用戶訪問權限
     * 用戶身份能夠訪問 訂單相關API
     * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        .antMatchers("/orders/**").hasRole("USER")    //用戶權限
        .antMatchers("/users/**").hasRole("ADMIN")    //管理員權限
        .and()
        .formLogin()
        .loginPage("/login")    //跳轉登陸頁面的控制器,該地址要保證和表單提交的地址一致!
        .permitAll()
        .and()
        .logout()
        .permitAll()
        .and()
        .csrf().disable();        //暫時禁用CSRF,不然沒法提交表單
    }

    /**
     * 重寫該方法,添加自定義用戶
     * */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
        .withUser("admin").password("admin").roles("ADMIN","USER")
        .and()
        .withUser("terry").password("terry").roles("USER")
        .and()
        .withUser("larry").password("larry").roles("USER");
    }
    
}
  • 添加 login(get方式)控制器

即若是用戶沒有登陸就訪問受保護的資源,系統將會進行攔截,攔截以後會請求/login(get方式),而後通過咱們這個控制器跳轉到DIY登陸頁面。

@Controller
@RequestMapping("/")
public class IndexController {

    @GetMapping("/login")
    public String login(Model model, @RequestParam(value = "error", required = false) String error) {
        if (error != null) {
            model.addAttribute("error", "用戶名或密碼錯誤");
        }
        return "forward:/login_page.html";
    }
}
  • 登陸頁面 (login_page.html)

注意:這裏表單的action爲 /login 提交方式爲POST

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陸頁面</title>
</head>
<body>
<h2>自定義登陸頁面</h2>
<hr>
<form action="/login" method="POST" name="f">
    用戶名<input type="text" name="username"/> <br>
    密碼 <input type="password" name="password"> <br>
    <input type="submit" value="登陸">
</form>
</body>
</html>

當須要登陸的時候,會跳轉到login_page.html中,至此完成自定義登陸頁面設置

clipboard.png

4 登陸後續操做

這裏我只是簡單處理了一下,經過SecurityContextHolder獲取目前登陸的用戶信息,而後將其放到session中(不建議如此處理)而後將頁面重定向到首頁中。

@Configuration
@EnableWebSecurity
public class AuthConfig extends WebSecurityConfigurerAdapter {
    
    /**
     * 重寫該方法,設定用戶訪問權限
     * 用戶身份能夠訪問 訂單相關API
     * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        .antMatchers("/orders/**").hasRole("USER")    //用戶權限
        .antMatchers("/users/**").hasRole("ADMIN")    //管理員權限
        .and()
        .formLogin()
        .loginPage("/login")    //跳轉登陸頁面的控制器,該地址要保證和表單提交的地址一致!
        .successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest arg0, HttpServletResponse arg1, Authentication arg2)
                    throws IOException, ServletException {
                Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                if (principal != null && principal instanceof UserDetails) {
                    UserDetails user = (UserDetails) principal;
                    System.out.println("loginUser:"+user.getUsername());
                    //維護在session中
                    arg0.getSession().setAttribute("userDetail", user);
                    arg1.sendRedirect("/");
                } 
            }
        })
        .permitAll()
        .and()
        .logout()
        .permitAll()
        .and()
        .csrf().disable();        //暫時禁用CSRF,不然沒法提交表單
    }

    /**
     * 重寫該方法,添加自定義用戶
     * */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
        .withUser("admin").password("admin").roles("ADMIN","USER")
        .and()
        .withUser("terry").password("terry").roles("USER")
        .and()
        .withUser("larry").password("larry").roles("USER");
    }
    
}

Mybatis Authentication

數據庫認證。也就是說要提供數據庫的支持,用戶信息和角色統一保存到數據庫中,這樣後期能夠提供註冊功能向數據庫中添加用戶信息。

1. 數據庫設計

設計了三張表,用戶表,角色表,用戶角色表,用戶與角色以前是多對多關係。外鍵維護在橋表中。
clipboard.png

建表語句以下

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for tbl_role
-- ----------------------------
DROP TABLE IF EXISTS `tbl_role`;
CREATE TABLE `tbl_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for tbl_user
-- ----------------------------
DROP TABLE IF EXISTS `tbl_user`;
CREATE TABLE `tbl_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `state` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `gender` varchar(255) DEFAULT NULL,
  `birth` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for tbl_user_role
-- ----------------------------
DROP TABLE IF EXISTS `tbl_user_role`;
CREATE TABLE `tbl_user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `role_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  KEY `role_id` (`role_id`),
  CONSTRAINT `tbl_user_role_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `tbl_role` (`id`),
  CONSTRAINT `tbl_user_role_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `tbl_user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

2. 提供對應的bean mapper service

都是基礎代碼,這裏就不詳細列出來。隨後提交到github上

clipboard.png

3. 自定義身份驗證

3.1 建立UserDetails的實現類

爲了使得咱們的用戶角色類能和security中的可以結合起來,須要從新建一個類MyUserDetails實現UserDetails接口。

MyUserDetails

/**
 * 自定義用戶身份信息
 * */
public class MyUserDetails implements UserDetails {
    // 用戶信息
    private User user;
    // 用戶角色
    private Collection<? extends GrantedAuthority> authorities;
    
    public MyUserDetails(User user, Collection<? extends GrantedAuthority> authorities) {
        super();
        this.user = user;
        this.authorities = authorities;
    }

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        
        return authorities;
    }

    @Override
    public String getPassword() {
        return this.user.getPassword();
    }

    @Override
    public String getUsername() {
        return this.user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return this.user.getState().equals(User.STATE_ACCOUNTEXPIRED);
    }

    @Override
    public boolean isAccountNonLocked() {
        return this.user.getState().equals(User.STATE_LOCK);
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return this.user.getState().equals(User.STATE_TOKENEXPIRED);
    }

    @Override
    public boolean isEnabled() {
        return this.user.getState().equals(User.STATE_NORMAL);
    }

}

用戶身份驗證 AuthUserDetailService

/**
 * 用戶身份認證服務類
 * */
@Service("userDetailsService")
public class AuthUserDetailService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserRoleMapper userRoleMapper;
    @Override
    public UserDetails loadUserByUsername(String name) 
            throws UsernameNotFoundException {
        UserDetails userDetails = null;
        try {
            User user = userMapper.findByUsername(name);
            if(user != null) {
                List<UserRole> urs = userRoleMapper.findByUserId(user.getId());
                Collection<GrantedAuthority> authorities = new ArrayList<>();
                for(UserRole ur : urs) {
                    String roleName = ur.getRole().getName();
                    SimpleGrantedAuthority grant = new SimpleGrantedAuthority(roleName);
                    authorities.add(grant);
                }
                //封裝自定義UserDetails類
                userDetails = new MyUserDetails(user, authorities);
            } else {
                throw new UsernameNotFoundException("該用戶不存在!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return userDetails;
    }

}

自定義認證服務

/**
 * 自定義認證服務
 * */
@Service("securityProvider")
public class SecurityProvider implements AuthenticationProvider {
    private  UserDetailsService userDetailsService;  
    public SecurityProvider(UserDetailsService userDetailsService) {  
        this.userDetailsService = userDetailsService;  
    }  
    @Override
    public Authentication authenticate(Authentication authenticate) throws AuthenticationException {
        UsernamePasswordAuthenticationToken token 
            = (UsernamePasswordAuthenticationToken) authenticate;
        String username = token.getName();
        UserDetails userDetails = null;
        
        if(username !=null) {
            userDetails = userDetailsService.loadUserByUsername(username);
        }
        System.out.println("$$"+userDetails);
        
        if(userDetails == null) {  
            throw new UsernameNotFoundException("用戶名/密碼無效");  
        }
        
        else if (!userDetails.isEnabled()){  
            System.out.println("jinyong用戶已被禁用");
            throw new DisabledException("用戶已被禁用");  
        }else if (!userDetails.isAccountNonExpired()) {  
            System.out.println("guoqi帳號已過時");
            throw new LockedException("帳號已過時");  
        }else if (!userDetails.isAccountNonLocked()) {  
            System.out.println("suoding帳號已被鎖定");
            throw new LockedException("帳號已被鎖定");  
        }else if (!userDetails.isCredentialsNonExpired()) {  
            System.out.println("pingzheng憑證已過時");
            throw new LockedException("憑證已過時");  
        }  
        
        String password = userDetails.getPassword();
         //與authentication裏面的credentials相比較  
        if(!password.equals(token.getCredentials())) {  
            throw new BadCredentialsException("Invalid username/password");  
        }  
        //受權  
        return new UsernamePasswordAuthenticationToken(userDetails, password,userDetails.getAuthorities());  
    }

    @Override
    public boolean supports(Class<?> authentication) {
         //返回true後纔會執行上面的authenticate方法,這步能確保authentication能正確轉換類型  
        return UsernamePasswordAuthenticationToken.class.equals(authentication);  
    }

}

核心認證配置

@Configuration
@EnableWebSecurity
public class AuthConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private AuthenticationProvider securityProvider;
    
    @Override
    protected UserDetailsService userDetailsService() {
        //自定義用戶信息類
        return this.userDetailsService;
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定義AuthenticationProvider  
        auth.authenticationProvider(securityProvider);
    }
    


    /**
     * 重寫該方法,設定用戶訪問權限
     * 用戶身份能夠訪問 訂單相關API
     * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        .antMatchers("/orders/**").hasRole("USER")    //用戶權限
        .antMatchers("/users/**").hasRole("ADMIN")    //管理員權限
        .and()
        .formLogin()
        .loginPage("/login")    //跳轉登陸頁面的控制器,該地址要保證和表單提交的地址一致!
        //成功處理
        .successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest arg0, HttpServletResponse arg1, Authentication arg2)
                    throws IOException, ServletException {
                Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                if (principal != null && principal instanceof UserDetails) {
                    UserDetails user = (UserDetails) principal;
                    System.out.println("loginUser:"+user.getUsername());
                    //維護在session中
                    arg0.getSession().setAttribute("userDetail", user);
                    arg1.sendRedirect("/");
                } 
            }
        })
        //失敗處理
        .failureHandler(new AuthenticationFailureHandler() {
            
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException)
                    throws IOException, ServletException {
                System.out.println("error"+authenticationException.getMessage());
                response.sendRedirect("/login");
            }
        })
        .permitAll()
        .and()
        .logout()
        .permitAll()
        .and()
        .csrf().disable();        //暫時禁用CSRF,不然沒法提交表單
    }
    
}

這時候就能夠準備經過數據庫用戶進行登陸。

用戶訪問資源-》security攔截-》跳轉到login-》提交表單-》securityProvider處理用戶信息-》藉助UserDetailsService 獲取用戶信息-》認證成功/失敗

代碼地址

相關文章
相關標籤/搜索