基于Spring Security前后端分离式项目解决方案
Spring Security 简介
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,是保护基于spring应用的实际标准。
Spring Security专注于为Java应用提供认证和授权,并且可以轻松扩展以满足自定义要求。
- 认证:访问者的身份验证,如登录
- 授权:访问者的权限控制,即可以干什么,不可以干什么
Spring Security的核心就是一组可配置的过滤器,请求访问资源之前被这些过滤器层层拦截,按照配置每一个过滤器都对请求执行自身的验证逻辑,通过则到下一个过滤器,如果一直通过,最终到达访问资源,其中任何一个未通过,则被拦截无法到达访问资源。
Spring Securit 直接提供了登录和退出功能,并提供了当前用户信息的模板。
Spring Security的应用方案
技术框架
后端
Spring Boot 、Spring Security 、MyBatis
前端
vue-cli 、vue、axios、elementui
基本思路
后端
- 支持跨域
- 禁用CSRF(如果不禁用CSRF,session不易跟踪)
- 密码在数据库中加密存储
- 认证成功或失败后、未认证被拦截后、未授权被拦截后 和 退出系统后一律向前端发送如下示例格式json串的结果数据。
{ "authorized":true,//是否已授权 "logined":true,//是否已登录 "message":"信息",//信息 "success":true //是否成功 }
前端
- 自定义登录页,不使用Spring Security提供的登录页
- 前端对处理代码进行封装,配合处理后端返回认证授权验证结果数据
后端具体实现
引入Spring Security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
建立安全配置类
在web应用中,Spring Security配置类需要继承WebSecurityConfigurerAdapter,重写其中的配置方法,主要的核心配置都在配置方法中。
-
配置类的总体结构
-
配置密码编码器具体代码
//配置密码编码器 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
BCryptPasswordEncoder是Spring Security提供的使用哈希算法结合盐值(盐值即一个安全随机数)加密器,该类对同一明文每次加密都不一样,哈希又是一种不可逆算法,所以密码认证时需要使用相同的方式对待校验的明文进行加密,然后比较这两个密文来进行验证。
-
注入安全Dao的具体代码
//安全Dao负责从数据库中获取认证和授权等数据 private final SecurityDao securityDao; //构造方法 public SecurityConfig(SecurityDao securityDao) { this.securityDao = securityDao; }
SecurityDao并非Spring Security提供的,是自定义的访问数据库的对象。
-
配置UserDetailsService对象的具体代码
UserDetailsService接口由Spring Security 提供,其作用为根据用户名(账号)加载当前用户信息,定义如下:public interface UserDetailsService{ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException }
当前用户信息UserDetials也是Spring Security 提供的接口 ,定义如下:
public interface UserDetails{ public Collection<? extends GrantedAuthority> getAuthorities(); //获取当前用户的权限 public String getPassword(); //获取当前用户的密码,此处获取的密码应当加密形式 public String getUsername(); //获取当前用户名(账号) public boolean isAccountNonExpired(); //当前用户是否未过期 public boolean isAccountNonLocked(); //当前用户是否未锁定 public boolean isCredentialsNonExpired(); //当前用户凭证(密码)是否未过期 public boolean isEnabled(); //当前用户是否可用 }
配置UserDetailsService对象的作用有两个,一个是提供自定义UserDetailsService的实现并作为spring的bean对象,另一个是提供UserDetails 实现,具体代码如下:
//配置UserDetailsService对象,用于用户获取当前用户信息 @Bean public UserDetailsService userDetailsService(SecurityDao securityDao) { /* 匿名实现UserDetailsService接口, 该接口中仅有一个方法public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException, 此方法的功能是依据登录用户名(可以理解为账号)加载当前用户信息,UserDetials接口表示当前用户信息 */ return userId -> { User user = securityDao.findUserByUserId(userId);//根据账号从数据库中查询用户信息 if (user == null) throw new UsernameNotFoundException("账号不正确!"); //根据账号从数据库中权限编号的集合 List<String> moduleIdList = securityDao.findModuleIdListByUserId(userId); List<GrantedAuthority> authorityList = new ArrayList<>();//权限集合 String authorityPattern = "ROLE_{0}";//在Spring Security中,权限的名称格式为“ROLE_XXX” //将权限编号转换为“ROLE_权限编号”的形式,然后封装为SimpleGrantedAuthority对象放入集合中 for (String moduleId : moduleIdList) { authorityList.add(new SimpleGrantedAuthority(MessageFormat.format(authorityPattern, moduleId))); } //CurrUser是一个自定义的类实现了UserDetails接口 return new CurrUser( user.getU_id(), user.getU_name(), user.getU_pwd(), user.getU_status().equals(DataStatusEnum.已启用.getCode()), authorityList ); }; }
上述配置中的CurrUser是自定义的UserDetails实现,具体代码如下:
public class CurrUser implements UserDetails { private String username;//账号 private String factname;//姓名 private String password; private boolean enabled; private Collection<? extends GrantedAuthority> authorities; public CurrUser() { } public CurrUser(String username, String factname, String password, boolean enabled, Collection<? extends GrantedAuthority> authorities) { this.username = username; this.factname = factname; this.password = password; this.enabled = enabled; this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } public String getFactname(){ return factname; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } }
-
核心安全配置
重写config(HttpSecurity)方法,其中写核心的安全配置,配置内容概括如下:
(1)通过查询数据库,将模块地址和相应权限编号对应起来,访问某个模块必须要有相应的权限;
(2)其它地址请求需要认证;
(3)配置登录,包括登录处理地址、登录账号密码参数名称、登录成功/失败的响应,以及允许所有请求访问登录相关地址(接口)等;
(4)配置退出,包括退出地址 和 退出成功的响应;
(5)配置异常处理,包括未认证异常和未授权异常
(6)配置允许跨域
(7)配置禁用防CSRF攻击具体配置代码如下:
//重写WebSecurityConfigurerAdapter的configure(HttpSeeurity)方法,核心的配置都在此方法中 @Override protected void configure(HttpSecurity http) throws Exception { //证授权配置-开始 String antUrlPattern = "{0}/**";//地址模式 List<Module> moduleList = securityDao.findModuleList();//获得所有权限 ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorize = http.authorizeRequests(); for (Module module : moduleList) { //为每一个地址模式匹配一个权限(角色) authorize .antMatchers(MessageFormat.format(antUrlPattern, module.getM_url())) .hasRole(module.getM_id().toString()); } authorize .anyRequest().authenticated()//其它请求需要认证 .and()//此方法返回ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry对象 //以下登录配置开始 .formLogin() .loginProcessingUrl("/login")//登录处理地址 .usernameParameter("u_id")//定义登录时,用户名的参数名,默认为 username .passwordParameter("u_pwd")//定义登录时,密码的参数名,默认为 password .successHandler((req, resp, authentication) -> { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write(JSON.toJSONString(Result.success("登录成功"))); out.flush(); })//登录成功的处理器 .failureHandler((req, resp, exception) -> { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write(JSON.toJSONString(Result.fail("登录失败!"))); out.flush(); })//登录失败的处理器 .permitAll();//登录相关访问地址一律放行 //以上登录配置 //认证授权配置-结束 http //以下退出配置 .logout() .logoutUrl("/logout")//退出地址 .logoutSuccessHandler((req, resp, authentication) -> { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write(JSON.toJSONString(Result.success("您已成功退出系统!"))); out.flush(); })//退出成功的处理器 .permitAll()//退出地址一律放行 //以上退出配置 .and() //返回HttpSecurity对象 //以下异常处理配置 .exceptionHandling() //未认证用户访问需要认证资源异常处理 .authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> { httpServletResponse.setContentType("application;charset=UTF-8"); PrintWriter out = httpServletResponse.getWriter(); out.print(JSON.toJSONString(Result.unlogined())); out.flush(); }) //认证用户访问资源权限不足异常处理 .accessDeniedHandler((httpServletRequest, httpServletResponse, e) -> { httpServletResponse.setContentType("application;charset=UTF-8"); PrintWriter out = httpServletResponse.getWriter(); out.print(JSON.toJSONString(Result.unauthorized())); out.flush(); }) //以上异常处理配置 .and() .cors()//允许跨域,如果springboot/springmvc已有跨域配置,自动采用springboot/springmvc跨域配置 .and() //禁用防CSRF攻击 .csrf().disable(); }
当前用户信息的获取
在spring mvc中,可以通过在控制器处理方法上定义Principal类型的参数获得经过认证的当前用户信息,示例如下:
@GetMapping("/curruser")
public CurrUser currUser(Principal principal){
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken)principal;
CurrUser currUser = (CurrUser) token.getPrincipal();
return currUser;
}
前端具体实现
- 登录页向登录处理地址(该地址已在服务器配置)以form-data方式发送post请求;
- 封装js统一处理授权认证响应。