接着上一章節,咱們在這一章種討論如何在現有的ssm框架中加入security機制,說白了,就是爲咱們項目提供身份驗證的功能。現有的需求中大多項目都沒法脫離登陸註冊功能。若是開發時每一個模塊提供一個登陸註冊功能,整個項目就會臃腫不堪,單點登陸也就應用而生了。至於OAuth2與springBoot的結合咱們在隨後章節討論,這一章節討論security機制的簡單應用。
基於內存的身份認證功能。也就是說身份信息是保存到內存中。這種方式瞭解爲主,在實際開發中使用較少。html
須要的依賴有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
哈,是否是有些意外,咱們就沒作什麼事情,居然具備受權的功能了,這是security默認幫咱們實現的功能,那麼用戶名密碼是什麼呢? 用戶名默認爲user,密碼在啓動項目的時候會打印到控制檯。web
若是咱們直接點擊取消,提示未受權異常。spring
刷新頁面後進行登陸。輸入user/console中密碼,出現以下錯誤,不過這個錯誤咱們是能理解的,404找不到,說明沒有配置服務。sql
在默認受權管理中若是咱們想添加用戶改怎麼辦?若是咱們想自定義登陸頁面怎麼辦?若是咱們想自定義攔截怎麼辦?數據庫
實際上咱們項目之全部具備受權功能,是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
當用戶名密碼輸入錯誤的時候,出現如下界面
當用戶名密碼輸入正確的時候,是能夠繼續訪問服務,因爲咱們還麼有提供任何服務,全部均會出現404異常。
訂單控制器 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"); } }
重啓服務進行登陸
默認狀況下,當用戶沒有登陸就去訪問受保護資源時,系統會默認請求/login(get方式),這時重定向到登陸頁(spring security自帶)。當輸入用戶名密碼點擊登陸按鈕的時候,系統會請求/login(post方式)。如今咱們但願自定義登陸頁面(默認的登陸頁面很醜),可是身份校驗仍是但願由security來進行。這時候咱們只須要將登陸頁面重定向到咱們自定義頁面便可,這時候DIY表單,可是在這裏切記一點。登陸頁面重定向的地址和表單提交的地址務必一致!
在原來的基礎上擴展了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方式),而後通過咱們這個控制器跳轉到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"; } }
注意:這裏表單的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中,至此完成自定義登陸頁面設置
這裏我只是簡單處理了一下,經過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"); } }
數據庫認證。也就是說要提供數據庫的支持,用戶信息和角色統一保存到數據庫中,這樣後期能夠提供註冊功能向數據庫中添加用戶信息。
設計了三張表,用戶表,角色表,用戶角色表,用戶與角色以前是多對多關係。外鍵維護在橋表中。
建表語句以下
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;
都是基礎代碼,這裏就不詳細列出來。隨後提交到github上
爲了使得咱們的用戶角色類能和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 獲取用戶信息-》認證成功/失敗