
,spring security 會默認使用一個用戶名為:user 的用戶,密碼就是 啟動的時候生成的(通過控制台console中查看),如圖
然后在用戶名中輸入:user 密碼框中輸入 上面的密碼 ,之后就可以正常訪問之前URL了。很顯然這根本不是我們想要的,接下來我們需要一步一步的改造。
改造1 使用頁面表單登錄
WebSecurityConfigurerAdapter ,
重寫
configure
方法。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override</br> </span><span style="color: #0000ff;">protected</span> <span style="color: #0000ff;">void</span> configure(HttpSecurity http) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> Exception {</br> </span><span style="color: #008000;">//</span><span style="color: #008000;"> TODO Auto-generated method stub</br> </span><span style="color: #008000;">//</span><span style="color: #008000;">super.configure(http);</span></br>
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"></br> <button type="submit" <span style="color: #0000ff;">class</span>="btn btn-lg btn-primary btn-block" >登錄</button></br> </td></br> </tr></br> </table></br> </form></pre>
<!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");}</span></pre>
還有種方法 就是 重寫 另外一種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 </span><span style="color: #0000ff;">public</span> UserDetails loadUserByUsername(String username) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> UsernameNotFoundException { </span><span style="color: #008000;">//</span><span style="color: #008000;"> TODO Auto-generated method stub </span><span style="color: #008000;">//</span><span style="color: #008000;">這里可以可以通過username(登錄時輸入的用戶名)然后到數據庫中找到對應的用戶信息,並構建成我們自己的UserInfo來返回。</span> <span style="color: #0000ff;">return</span> <span style="color: #0000ff;">null</span><span style="color: #000000;">; }
}
// TODO Auto-generated method stub</span><span style="color: #008000;">//</span><span style="color: #008000;">這里可以通過數據庫來查找到實際的用戶信息,這里我們先模擬下,后續我們用數據庫來實現</span> <span style="color: #0000ff;">if</span>(username.equals("admin"<span style="color: #000000;">)) { </span><span style="color: #008000;">//</span><span style="color: #008000;">假設返回的用戶信息如下;</span> UserInfo userInfo=<span style="color: #0000ff;">new</span> UserInfo("admin", "123456", "ROLE_ADMIN", <span style="color: #0000ff;">true</span>,<span style="color: #0000ff;">true</span>,<span style="color: #0000ff;">true</span>, <span style="color: #0000ff;">true</span><span style="color: #000000;">); </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> userInfo; } </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">null</span>;</pre>

@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 </span><span style="color: #0000ff;">protected</span> <span style="color: #0000ff;">void</span> configure(AuthenticationManagerBuilder auth) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> Exception { </span><span style="color: #008000;">//</span><span style="color: #008000;"> TODO Auto-generated method stub</span>
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{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));@Autowired </span><span style="color: #0000ff;">private</span><span style="color: #000000;"> ObjectMapper objectMapper; @Override </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) </span><span style="color: #0000ff;">throws</span><span style="color: #000000;"> IOException, ServletException { </span><span style="color: #008000;">//</span><span style="color: #008000;">什么都不做的話,那就直接調用父類的方法</span> <span style="color: #0000ff;">super</span><span style="color: #000000;">.onAuthenticationSuccess(request, response, authentication); </span><span style="color: #008000;">//</span><span style="color: #008000;">這里可以根據實際情況,來確定是跳轉到頁面或者json格式。 </span><span style="color: #008000;">//</span><span style="color: #008000;">如果是返回json格式,那么我們這么寫</span>
</span><span style="color: #008000;">//</span><span style="color: #008000;">如果是要跳轉到某個頁面的,比如我們的那個whoim的則</span> <span style="color: #0000ff;">new</span> DefaultRedirectStrategy().sendRedirect(request, response, "/whoim"<span style="color: #000000;">); }
}

//登錄失敗的 @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 </span><span style="color: #0000ff;">protected</span> <span style="color: #0000ff;">void</span> configure(HttpSecurity http) <span style="color: #0000ff;">throws</span><span style="color: #000000;"> Exception { </span><span style="color: #008000;">//</span><span style="color: #008000;"> TODO Auto-generated method stub </span><span style="color: #008000;">//</span><span style="color: #008000;">super.configure(http);</span>
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<span style="color: #008000;">/**</span><span style="color: #008000;"> * 記住我功能的token存取器配置 * </span><span style="color: #808080;">@return</span> <span style="color: #008000;">*/</span><span style="color: #000000;"> @Bean </span><span style="color: #0000ff;">public</span><span style="color: #000000;"> PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> tokenRepository; }</span></pre>
修改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();


