,spring security 會默認使用一個用戶名為:user 的用戶,密碼就是 啟動的時候生成的(通過控制台console中查看),如圖

然后在用戶名中輸入:user 密碼框中輸入 上面的密碼 ,之后就可以正常訪問之前URL了。很顯然這根本不是我們想要的,接下來我們需要一步一步的改造。
改造1 使用頁面表單登錄
WebSecurityConfigurerAdapter ,
重寫
configure
方法。
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub //super.configure(http); http .formLogin().loginPage("/login").loginProcessingUrl("/login/form").failureUrl("/login-error").permitAll() //表單登錄,permitAll()表示這個不需要驗證 登錄頁面,登錄失敗頁面 .and() .authorizeRequests().anyRequest().authenticated() .and() .csrf().disable(); } }
<form class="form-signin" action="/login/form" method="post"> <h2 class="form-signin-heading">用戶登錄</h2> <table> <tr> <td>用戶名:</td> <td><input type="text" name="username" class="form-control" placeholder="請輸入用戶名"/></td> </tr> <tr> <td>密碼:</td> <td><input type="password" name="password" class="form-control" placeholder="請輸入密碼" /></td> </tr> <tr> <td colspan="2"> <button type="submit" class="btn btn-lg btn-primary btn-block" >登錄</button> </td> </tr> </table> </form>
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>用戶登錄</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" />
<link rel="stylesheet" href="/css/sign.css" />
</head>
<body>
<h3>用戶名或密碼錯誤</h3>
</body>
</html>
我們用一個測試的RestController來測試
@RestController public class HelloWorldController { @RequestMapping("/hello") public String helloWorld() { return "spring security hello world"; } }
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER"); }
我們也照樣,這是把用戶名改成 admin 密碼改成 123456 roles是該用戶的角色,我們后面再細說。
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("admin").password("123456").roles("USER"); }
還有種方法 就是 重寫 另外一種configure(AuthenticationManagerBuilder auth) 方法,這個和上面那個方法的作用是一樣的。選其一就可。
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO Auto-generated method stub auth .inMemoryAuthentication() .withUser("admin").password("123456").roles("USER") .and() .withUser("test").password("test123").roles("ADMIN"); }
程序運行起來,這時用我們自己的用戶名和密碼 輸入 admin 和123456 就可以了。
public class UserInfo implements Serializable, UserDetails { /** * */ private static final long serialVersionUID = 1L; private String username; private String password; private String role; private boolean accountNonExpired; private boolean accountNonLocked; private boolean credentialsNonExpired; private boolean enabled; public UserInfo(String username, String password, String role, boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled) { // TODO Auto-generated constructor stub this.username = username; this.password = password; this.role = role; this.accountNonExpired = accountNonExpired; this.accountNonLocked = accountNonLocked; this.credentialsNonExpired = credentialsNonExpired; this.enabled = enabled; } // 這是權限 @Override public Collection<? extends GrantedAuthority> getAuthorities() { // TODO Auto-generated method stub return AuthorityUtils.commaSeparatedStringToAuthorityList(role); } @Override public String getPassword() { // TODO Auto-generated method stub return password; } @Override public String getUsername() { // TODO Auto-generated method stub return username; } @Override public boolean isAccountNonExpired() { // TODO Auto-generated method stub return accountNonExpired; } @Override public boolean isAccountNonLocked() { // TODO Auto-generated method stub return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { // TODO Auto-generated method stub return credentialsNonExpired; } @Override public boolean isEnabled() { // TODO Auto-generated method stub return enabled; } }
然后實現第2個類 UserService 來返回這個UserInfo的對象實例
@Component public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // TODO Auto-generated method stub //這里可以可以通過username(登錄時輸入的用戶名)然后到數據庫中找到對應的用戶信息,並構建成我們自己的UserInfo來返回。 return null; } } // TODO Auto-generated method stub //這里可以通過數據庫來查找到實際的用戶信息,這里我們先模擬下,后續我們用數據庫來實現 if(username.equals("admin")) { //假設返回的用戶信息如下; UserInfo userInfo=new UserInfo("admin", "123456", "ROLE_ADMIN", true,true,true, true); return userInfo; } return null;
@Component public class MyAuthenticationProvider implements AuthenticationProvider { /** * 注入我們自己定義的用戶信息獲取對象 */ @Autowired private UserDetailsService userDetailService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // TODO Auto-generated method stub String userName = authentication.getName();// 這個獲取表單輸入中返回的用戶名; String password = (String) authentication.getPrincipal();// 這個是表單中輸入的密碼; // 這里構建來判斷用戶是否存在和密碼是否正確 UserInfo userInfo = (UserInfo) userDetailService.loadUserByUsername(userName); // 這里調用我們的自己寫的獲取用戶的方法; if (userInfo == null) { throw new BadCredentialsException("用戶名不存在"); } // //這里我們還要判斷密碼是否正確,實際應用中,我們的密碼一般都會加密,以Md5加密為例 // Md5PasswordEncoder md5PasswordEncoder=new Md5PasswordEncoder(); // //這里第個參數,是salt // 就是加點鹽的意思,這樣的好處就是用戶的密碼如果都是123456,由於鹽的不同,密碼也是不一樣的,就不用怕相同密碼泄漏之后,不會批量被破解。 // String encodePwd=md5PasswordEncoder.encodePassword(password, userName); // //這里判斷密碼正確與否 // if(!userInfo.getPassword().equals(encodePwd)) // { // throw new BadCredentialsException("密碼不正確"); // } // //這里還可以加一些其他信息的判斷,比如用戶賬號已停用等判斷,這里為了方便我接下去的判斷,我就不用加密了。 // // if (!userInfo.getPassword().equals("123456")) { throw new BadCredentialsException("密碼不正確"); } Collection<? extends GrantedAuthority> authorities = userInfo.getAuthorities(); // 構建返回的用戶登錄成功的token return new UsernamePasswordAuthenticationToken(userInfo, password, authorities); } @Override public boolean supports(Class<?> authentication) { // TODO Auto-generated method stub // 這里直接改成retrun true;表示是支持這個執行 return true; } }
到此為止,我們的用戶信息的獲取,校驗部分已經完成了。接下來要讓它起作用,則我們需要在配置文件中修改,讓他起作用。回到我的SecurityConfig代碼文件,修改如下:
@Autowired private AuthenticationProvider provider; //注入我們自己的AuthenticationProvider @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO Auto-generated method stub auth.authenticationProvider(provider); // auth // .inMemoryAuthentication() // .withUser("admin").password("123456").roles("USER") // .and() // .withUser("test").password("test123").roles("ADMIN"); }
@RequestMapping("/whoim") public Object whoIm() { return SecurityContextHolder.getContext().getAuthentication().getPrincipal(); }
我們運行,直接反問 /whoim ,則直接跳轉到登錄頁面,我們驗證過之后,再訪問此url,結果如下:

到這里,我們自定義的登錄已經成功了。
改造3、自定義登錄成功和失敗的處理邏輯
//處理登錄成功的。 @Component("myAuthenticationSuccessHandler") public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{ @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //什么都不做的話,那就直接調用父類的方法 super.onAuthenticationSuccess(request, response, authentication); //這里可以根據實際情況,來確定是跳轉到頁面或者json格式。 //如果是返回json格式,那么我們這么寫 Map<String,String> map=new HashMap<>(); map.put("code", "200"); map.put("msg", "登錄成功"); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(map)); //如果是要跳轉到某個頁面的,比如我們的那個whoim的則 new DefaultRedirectStrategy().sendRedirect(request, response, "/whoim"); } }
//登錄失敗的 @Component("myAuthenticationFailHander") public class MyAuthenticationFailHander extends SimpleUrlAuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; private Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { // TODO Auto-generated method stub logger.info("登錄失敗"); //以Json格式返回 Map<String,String> map=new HashMap<>(); map.put("code", "201"); map.put("msg", "登錄失敗"); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(map)); } }
@Autowired private AuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private AuthenticationFailureHandler myAuthenticationFailHander; @Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub //super.configure(http); http .formLogin().loginPage("/login").loginProcessingUrl("/login/form") .successHandler(myAuthenticationSuccessHandler) .failureHandler(myAuthenticationFailHander) .permitAll() //表單登錄,permitAll()表示這個不需要驗證 登錄頁面,登錄失敗頁面 .and() .authorizeRequests().anyRequest().authenticated() .and() .csrf().disable(); }
進行測試,我們先返回json格式的(登錄成功和失敗的)

改成跳轉到默認頁面

改造4、添加權限控制
@Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub //super.configure(http); http .formLogin().loginPage("/login").loginProcessingUrl("/login/form") .successHandler(myAuthenticationSuccessHandler) .failureHandler(myAuthenticationFailHander) .permitAll() //表單登錄,permitAll()表示這個不需要驗證 登錄頁面,登錄失敗頁面 .and() .authorizeRequests() .antMatchers("/index").permitAll() //這就表示 /index這個頁面不需要權限認證,所有人都可以訪問 .anyRequest().authenticated() .and() .csrf().disable(); }
http .formLogin().loginPage("/login").loginProcessingUrl("/login/form") .successHandler(myAuthenticationSuccessHandler) .failureHandler(myAuthenticationFailHander) .permitAll() //表單登錄,permitAll()表示這個不需要驗證 登錄頁面,登錄失敗頁面 .and() .authorizeRequests() .antMatchers("/index").permitAll() .antMatchers("/whoim").hasRole("ADMIN") //這就表示/whoim的這個資源需要有ROLE_ADMIN的這個角色才能訪問。不然就會提示拒絕訪問 .anyRequest().authenticated() //必須經過認證以后才能訪問 .and() .csrf().disable();
這個用戶的角色哪里來,就是我們自己的UserDetailsService中返回的用戶信息中的角色權限信息,這里需要注意一下就是 .hasRole("ADMIN"),那么給用戶的角色時就要用:ROLE_ADMIN
/** * 返回權限驗證的接口 * * */ public interface RbacService { boolean hasPermission(HttpServletRequest request,Authentication authentication); } @Component("rbacService") public class RbacServiceImpl implements RbacService { private AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public boolean hasPermission(HttpServletRequest request, Authentication authentication) { Object principal = authentication.getPrincipal(); boolean hasPermission = false; if (principal instanceof UserDetails) { //首先判斷先當前用戶是否是我們UserDetails對象。 String userName = ((UserDetails) principal).getUsername(); Set<String> urls = new HashSet<>(); // 數據庫讀取 //讀取用戶所擁有權限的所有URL urls.add("/whoim"); // 注意這里不能用equal來判斷,因為有些URL是有參數的,所以要用AntPathMatcher來比較 for (String url : urls) { if (antPathMatcher.match(url, request.getRequestURI())) { hasPermission = true; break; } } } return hasPermission; } }
然后在Security的配置項中添加自定義的權限表達式就可以了。
@Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub //super.configure(http); http .formLogin().loginPage("/login").loginProcessingUrl("/login/form") .successHandler(myAuthenticationSuccessHandler) .failureHandler(myAuthenticationFailHander) .permitAll() //表單登錄,permitAll()表示這個不需要驗證 登錄頁面,登錄失敗頁面 .and() .authorizeRequests() // .antMatchers("/index").permitAll() // .antMatchers("/whoim").hasRole("ADMIN") // .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN") // .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER") .anyRequest().access("@rbacService.hasPermission(request,authentication)") //必須經過認證以后才能訪問 .and() .csrf().disable(); }
其中 @rbacService 就是我們自己聲明的bean,在RbacServiceImpl實現類的頭部注解中。
改造5、記住我的功能Remeber me
CREATE TABLE persistent_logins ( username VARCHAR(64) NOT NULL, series VARCHAR(64) NOT NULL, token VARCHAR(64) NOT NULL, last_used TIMESTAMP NOT NULL, PRIMARY KEY (series) );
然后,配置好token 的存儲 及數據源
@Autowired private DataSource dataSource; //是在application.properites /** * 記住我功能的token存取器配置 * @return */ @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); return tokenRepository; }
修改Security配置
@Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub //super.configure(http); http .formLogin().loginPage("/login").loginProcessingUrl("/login/form") .successHandler(myAuthenticationSuccessHandler) .failureHandler(myAuthenticationFailHander) .permitAll() //表單登錄,permitAll()表示這個不需要驗證 登錄頁面,登錄失敗頁面 .and() .rememberMe() .rememberMeParameter("remember-me").userDetailsService(userDetailsService) .tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(60) .and() .authorizeRequests() // .antMatchers("/index").permitAll() // .antMatchers("/whoim").hasRole("ADMIN") // .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN") // .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER") .anyRequest().access("@rbacService.hasPermission(request,authentication)") //必須經過認證以后才能訪問 .and() .csrf().disable();
