生活加油:摘一句子:
“我希望自己能写这样的诗。我希望自己也是一颗星星。如果我会发光,就不必害怕黑暗。如果我自己是那么美好,那么一切恐惧就可以烟消云散。于是我开始存下了一点希望—如果我能做到,那么我就战胜了寂寞的命运。”
-----------------------------王小波《我在荒岛上迎接黎明》
————————————————
嗯,刚学的时候,一大堆,明白是配置了啥,不知道为啥那么配,嗯,今天把配置思想和小伙伴分享,是很简单的那种,不足之处请留言.
SpringSecurity的配置基于WebSecurityConfigurerAdapter的实现类,我们这里主要讲基本配置,即configure(HttpSecurity http)方法的配置,其实大都有默认值,我们可以直接用默认值,也可以自己设置.
私以为简单配置三点:
- 认证(SpringSecurity在第一次登录做认证,这时候只进行认证,不进行授权(鉴权处理))
- 表单认证:涉及到的配置包括前端登录请求的url,登录失败,登录成功返回的页面url,已及表单返回字段的用户名密码K的设置,还有登陆成功回调处理和登录失败回调处理.这里要注意登录数据是通过key/value的形式来传递.
- 自定义认证,(验证码):自定义验证采用,在自定义过滤器的方式,在最开始插入一个过滤器链,需要自定义异常,自定义失败登录失败处理器,注意这里的失败处理器要和表单登录使用同一个失败处理器.
- 授权(基于RBAC权限控制):
- 授权这样分析基于RBAC的权限控制,即用户分配角色,角色分配权限.需要配置根据访问路径得到需要的角色的处理类(FilterlnvocationSecurityrMetadataSource),根据返回角色进行鉴权处理类(AccessDecisionManager) , 同时判断是否登录,还需要配置认证失败的处理器.
- 注销登录
- 注销登录默认的url为"/logout".需要配置,注销登录处理器
配置图:
SpringSecurity简单配置图:
下面就上面讲的代码分析
一.认证:
1.configure(HttpSecurity http)方法的配置:
package com.liruilong.hros.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.liruilong.hros.filter.VerifyCodeFilter; import com.liruilong.hros.model.Hr; import com.liruilong.hros.model.RespBean; import com.liruilong.hros.service.HrService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.*; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @Description : * @Author: Liruilong * @Date: 2019/12/18 19:11 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired HrService hrService; @Autowired CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource; @Autowired CustomUrlDecisionManager customUrlDecisionManager; @Autowired VerifyCodeFilter verifyCodeFilter ; @Autowired MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class) .authorizeRequests() //.anyRequest().authenticated() //所有请求的都会经过这进行鉴权处理。返回当前请求需要的角色。 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource); object.setAccessDecisionManager(customUrlDecisionManager); return object; } }) .and().formLogin().usernameParameter("username").passwordParameter("password") //设置登录请求的url路径 .loginProcessingUrl("/doLogin") /*需要身份验证时,将浏览器重定向到/ login 我们负责在请求/ login时呈现登录页面 当身份验证尝试失败时,将浏览器重定向到/ login?error(因为我们没有另外指定) 当请求/ login?error时,我们负责呈现失败页面 成功注销后,将浏览器重定向到/ login?logout(因为我们没有另外指定) 我们负责在请求/ login?logout时呈现注销确认页面*/ .loginPage("/login") //登录成功回调 .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); Hr hr = (Hr) authentication.getPrincipal(); //密码不回传 hr.setPassword(null); RespBean ok = RespBean.ok("登录成功!", hr); //将hr转化为Sting String s = new ObjectMapper().writeValueAsString(ok); out.write(s); out.flush(); out.close(); } }) //登失败回调 .failureHandler(myAuthenticationFailureHandler) //相关的接口直接返回 .permitAll().and().logout() //注销登录 .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功!"))); out.flush(); out.close(); } }) .permitAll().and().csrf().disable().exceptionHandling() //没有认证时,在这里处理结果,不要重定向 .authenticationEntryPoint( //myAuthenticationEntryPoint; 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(); } }); } @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(hrService); } /** * @Author Liruilong * @Description 放行的请求路径 * @Date 19:25 2020/2/7 * @Param [web] * @return void **/ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/auth/code","/login","/css/**","/js/**", "/index.html", "/img/**", "/fonts/**","/favicon.ico"); } }
2.失败处理器定义:
package com.liruilong.hros.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.liruilong.hros.Exception.ValidateCodeException; import com.liruilong.hros.model.RespBean; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.*; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @Description : * @Author: Liruilong * @Date: 2020/2/11 23:08 */ @Component public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); RespBean respBean = RespBean.error(e.getMessage()); // 验证码自定义异常的处理 if (e instanceof ValidateCodeException){ respBean.setMsg(e.getMessage()); //Security内置的异常处理 }else if (e instanceof LockedException) { respBean.setMsg("账户被锁定请联系管理员!"); } else if (e instanceof CredentialsExpiredException) { respBean.setMsg("密码过期请联系管理员!"); } else if (e instanceof AccountExpiredException) { respBean.setMsg("账户过期请联系管理员!"); } else if (e instanceof DisabledException) { respBean.setMsg("账户被禁用请联系管理员!"); } else if (e instanceof BadCredentialsException) { respBean.setMsg("用户名密码输入错误,请重新输入!"); } //将hr转化为Sting out.write(new ObjectMapper().writeValueAsString(respBean)); out.flush(); out.close(); } @Bean public MyAuthenticationFailureHandler getMyAuthenticationFailureHandler(){ return new MyAuthenticationFailureHandler(); } }
3.前端将JSON数据转换为K-V形式:
//post请求的封装K-v形式 let base = ''; export const postKeyValueRequest = (url, params) => { return axios({ method: 'post', url: `${base}${url}`, data: params, transformRequest: [function (data) { let ret = '' for (let it in data) { ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&' } return ret }], headers: { 'Content-Type': 'application/x-www-form-urlencoded', } }); }
4.自定义过滤器:
package com.liruilong.hros.filter; import com.liruilong.hros.Exception.ValidateCodeException; import com.liruilong.hros.config.MyAuthenticationFailureHandler; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Description : * @Author: Liruilong * @Date: 2020/2/7 19:39 */ @Component public class VerifyCodeFilter extends OncePerRequestFilter { @Bean public VerifyCodeFilter getVerifyCodeFilter() { return new VerifyCodeFilter(); } @Autowired MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (StringUtils.equals("/doLogin", request.getRequestURI()) && StringUtils.equalsIgnoreCase(request.getMethod(), "post")) { // 1. 进行验证码的校验 try { String requestCaptcha = request.getParameter("code"); if (requestCaptcha == null) { throw new ValidateCodeException("验证码不存在"); } String code = (String) request.getSession().getAttribute("yanzhengma"); if (StringUtils.isBlank(code)) { throw new ValidateCodeException("验证码过期!"); } code = code.equals("0.0") ? "0" : code; logger.info("开始校验验证码,生成的验证码为:" + code + " ,输入的验证码为:" + requestCaptcha); if (!StringUtils.equals(code, requestCaptcha)) { throw new ValidateCodeException("验证码不匹配"); } } catch (AuthenticationException e) { // 2. 捕获步骤1中校验出现异常,交给失败处理类进行进行处理 myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e); } finally { filterChain.doFilter(request, response); } } else { filterChain.doFilter(request, response); } } }
5.自定义异常:
package com.liruilong.hros.Exception; import org.springframework.security.core.AuthenticationException; /** * @Description : * @Author: Liruilong * @Date: 2020/2/8 7:24 */ public class ValidateCodeException extends AuthenticationException { public ValidateCodeException(String msg) { super(msg); } }
二.授权:
RBAC模型实体图
1.开发者自定义 FilterlnvocationSecurityrMetadataSource
package com.liruilong.hros.config; import com.liruilong.hros.mapper.MenuMapper; import com.liruilong.hros.model.Menu; import com.liruilong.hros.model.Role; import com.liruilong.hros.service.MenuService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import java.util.Collection; import java.util.List; /** * @Description : 权限处理,根据请求,分析需要的角色,该类的主要功能就是通过当前的请求地址,获取该地址需要的用户角色 * @Author: Liruilong * @Date: 2019/12/24 12:17 */ @Component public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired MenuService menuService; //路径比较工具 AntPathMatcher antPathMatcher = new AntPathMatcher(); /** * @return java.util.Collection<org.springframework.security.access.ConfigAttribute> * @Author Liruilong * @Description 当前请求需要的角色 * @Date 18:13 2019/12/24 * @Param [object] **/ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { //获取当前请求路径 String requestUrl = ((FilterInvocation) object).getRequestUrl(); //获取所有的菜单url路径 List<Menu> menus = menuService.getAllMenusWithRole(); for (Menu menu : menus) { if (antPathMatcher.match(menu.getUrl(), requestUrl)) { //拥有当前菜单权限的角色 List<Role> roles = menu.getRoles(); String[] strings = new String[roles.size()]; for (int i = 0; i < roles.size(); i++) { strings[i] = roles.get(i).getName(); } return SecurityConfig.createList(strings); } } // 没匹配上的资源都是登录 return SecurityConfig.createList("ROLE_LOGIN"); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return true; } }
2.自定义 AccessDecisionManager
package com.liruilong.hros.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; import java.util.Collection; /** * @Description : 判断当前用户是否具备菜单访问,当一个请求走完 FilterlnvocationSecurityMetadataSource 中的 getAttributes 方法后,接下来就会 来到 AccessDecisionManager 类中进行角色信息的比对 * @Author: Liruilong * @Date: 2019/12/24 19:12 */ @Component public class CustomUrlDecisionManager implements AccessDecisionManager { /** * @return void * @Author Liruilong * @Description decide 方法有三个参数, 第一个参数包含当前登录用户的信息; * 第二个参数则是一个 Filterlnvocation 对 象 ,可以 获 取当前请求对 象等; * 第 三个参 数就是 FilterlnvocationSecurityMetadataSource 中的 getAttributes 方法的返回值, 即当前请求 URL 所 需要的角色。 * @Date 18:28 2020/2/13 * @Param [authentication, object, configAttributes] **/ @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; } } 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; } } // authorities.stream().anyMatch((authority) ->authority.getAuthority().equals(attribute));
RBAC模型分析
3. 当然还需要用户类去实现UserDatails接口
package com.liruilong.hros.model; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreType; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class Hr implements UserDetails { private Integer id; private String name; private String phone; private String telephone; private String address; private Boolean enabled; private String username; private String password; private String userface; private String remark; private List<Role> roles; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name == null ? null : name.trim(); } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone == null ? null : phone.trim(); } public String getTelephone() { return telephone; } public void setTelephone(String telephone) { this.telephone = telephone == null ? null : telephone.trim(); } public String getAddress() { return address; } public void setAddress(String address) { this.address = address == null ? null : address.trim(); } public void setEnabled(Boolean enabled) { this.enabled = enabled; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username == null ? null : username.trim(); } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password == null ? null : password.trim(); } public String getUserface() { return userface; } public void setUserface(String userface) { this.userface = userface == null ? null : userface.trim(); } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark == null ? null : remark.trim(); } @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()); roles.stream().forEach( (role) ->authorities.add(new SimpleGrantedAuthority(role.getName()))); return authorities; } public List<Role> getRoles() { return roles; } public void setRoles(List<Role> roles) { this.roles = roles; } }
_______________________________________________________________________________________________________
嗯.以上就是对SpringSecurity配置的理解,不足之处请小伙伴留言/生活加油!