Spring Security登錄驗證
背景
本人最近微人事項目進行了學習,記錄一下學習的過程,總結一下。
首先,這個微人事項目是爲了事業單位的人事管理,包括用戶登錄的權限管理、員工基本資料的增刪查改,薪資管理、部門管理等等。
接下來總結下權限管理:
權限數據庫設計
這裏需要建立5個數據表,分別是資源表、角色表、用戶表、資源角色表和用戶角色表。
資源表menu
這是登錄之後前端界面可以顯示的模塊。
角色表role
認證採用的Spring-Security,所以,英文名需要以ROLE_開頭。
用戶表hr
該表統計用戶的基本信息。
資源角色表menu_role
這裏的mid
對應於資源表的id
,rid
對應於角色表的id
。這裏就是爲了實現根據登錄的用戶的角色動態顯示前端頁面的資源信息,即實現不同的用戶有不同的權限。
用戶角色表hr_role
這裏的hrid
對應於用戶表中的id
,rid
對應於角色表裏的id
,可以實現用戶多角色的功能。
角色與資源的關係
其中hr
用戶表實現了UserDetails
接口
除了用戶的基本信息外,
// 用於賬戶是否過期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 賬戶是否被鎖住
@Override
public boolean isAccountNonLocked() {
return true;
}
//密碼是否過期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//密碼是否被禁用
@Override
public boolean isEnabled() {
return enabled;
}
//一個用戶對應於多個角色,統計用戶的角色進行返回
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>(roles.size());
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
HrService
實現了UserDetailsService
。用於執行登錄。
重寫了loadUserByUsername
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
Hr hr = hrMapper.loadUserByUsername(username);
if(hr == null){
throw new UsernameNotFoundException("用戶名不存在!");
}
//設置當前用戶所具有的角色
hr.setRoles(hrMapper.getHrRolesById(hr.getId()));
return hr;
}
講一下登錄
FilterInvocationSecurityMetadataSource
,此類的功能是通過當前的請求地址,獲取該地址需要的角色。
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
List<Menu> menus = menuService.getAllMenusWithRole();
for (Menu menu : menus) {
if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
List<Role> roles = menu.getRoles();
String[] str = new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
str[i] = roles.get(i).getName();
}
return SecurityConfig.createList(str);
}
}
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
從getAttribute(Object o)
方法中的參數o
中提取出當前請求的URL,然後將這個請求和數據庫查詢出來的URL-pattern進行比較,看符合哪一個URL-pattern,就獲取到該URL-pattern所對應的角色。這時的角色可能有多個角色,需要進行遍歷,利用SecurityConfig.createList()
方法創建一個角色集合。當輸入的URL和數據庫中查詢的不對應,需要進行登錄ROLE_LOGIN
。
AccessDecisionManager
,從FilterInovationSecurityMetadataSource
中的getAttribute()
返回的集合會來到這裏。
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : configAttributes) {
String needRole = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("尚未登錄,請登錄!");
}else {
return;
}
}
//把已經被認證的的角色與needRole進行比較
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("權限不足,請聯繫管理員!");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
在這個類中有個decide()
方法,有3個參數,第一個參數保存了當前登錄用戶的角色信息,第3個參數是由FilterInvocationSecurityMetadataSource
中getAttributes()
方法傳來的,表示當前登錄用戶的角色是否能和數據庫查詢的角色相匹配。
當登錄用戶的角色和經過FilterInvocationSecurityMetadataSource
處理的角色匹配得上時,進行返回。
配置SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
HrService hrService;
@Autowired
CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
@Autowired
CustomUrlDecisionManager customUrlDecisionManager;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//認證管理器配置方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//和用戶相關的都進行配置
auth.userDetailsService(hrService);
}
// 核心過濾器配置方法
@Override
public void configure(WebSecurity web) throws Exception {
//忽略規則配置文件。
web.ignoring().antMatchers("/css/**","/js/**","/index.html","/img/**","/fonts/**","/favicon.ico","/verifyCode");
}
@Bean
LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
//登錄成功時
loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//設置Body的類型。
response.setContentType("application/json;charset=utf-8");
//寫入響應,獲取一個字符流。
PrintWriter out = response.getWriter();
//獲取授權後的用戶信息
Hr hr = (Hr) authentication.getPrincipal();
hr.setPassword(null);
RespBean ok = RespBean.ok("登錄成功!", hr);
String s = new ObjectMapper().writeValueAsString(ok);
out.write(s);
out.flush();
out.close();
}
});
//登錄失敗
loginFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
RespBean respBean = RespBean.error(exception.getMessage());
if (exception instanceof LockedException) {
respBean.setMsg("賬戶被鎖定,請聯繫管理員!");
} else if (exception instanceof CredentialsExpiredException) {
respBean.setMsg("密碼過期,請聯繫管理員!");
} else if (exception instanceof AccountExpiredException) {
respBean.setMsg("賬戶過期,請聯繫管理員!");
} else if (exception instanceof DisabledException) {
respBean.setMsg("賬戶被禁用,請聯繫管理員!");
} else if (exception instanceof BadCredentialsException) {
respBean.setMsg("用戶名或者密碼輸入錯誤,請重新輸入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
});
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setFilterProcessesUrl("/doLogin");
return loginFilter;
}
//安全過濾器鏈配置方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
return object;
}
})
.and()
.logout()
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(RespBean.ok("註銷成功!")));
out.flush();
out.close();
}
})
.permitAll()
.and()
.csrf().disable().exceptionHandling()
//沒有認證時,在這裏處理結果,不要重定向
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException authException) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(401);
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("訪問失敗!");
if (authException instanceof InsufficientAuthenticationException) {
respBean.setMsg("請求失敗,請聯繫管理員!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
});
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
在SecurityConfig
配置類中有LoginFilter
,Bean
對象。看看它都有哪些東西
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {//向服務器發送信息。
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//從session中獲取之前存入的驗證碼。
String verify_code = (String) request.getSession().getAttribute("verify_code");
//處理json類型的數據
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
Map<String, String> loginData = new HashMap<>();
try {
loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
}finally {
//把用戶輸入的驗證碼和session裏面的進行比較
String code = loginData.get("code");
checkCode(response, code, verify_code);
}
//得到用戶名
//得到密碼
String username = loginData.get(getUsernameParameter());
String password = loginData.get(getPasswordParameter());
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//身份驗證
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} else {
checkCode(response, request.getParameter("code"), verify_code);
return super.attemptAuthentication(request, response);
}
}
public void checkCode(HttpServletResponse resp, String code, String verify_code) {
if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) {
//驗證碼不正確
throw new AuthenticationServiceException("驗證碼不正確");
}
}
}