基于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

基本思路

后端
  1. 支持跨域
  2. 禁用CSRF(如果不禁用CSRF,session不易跟踪)
  3. 密码在数据库中加密存储
  4. 认证成功或失败后、未认证被拦截后、未授权被拦截后 和 退出系统后一律向前端发送如下示例格式json串的结果数据。
    {
    	"authorized":true,//是否已授权
    	"logined":true,//是否已登录
    	"message":"信息",//信息
    	"success":true //是否成功
    }
    
前端
  1. 自定义登录页,不使用Spring Security提供的登录页
  2. 前端对处理代码进行封装,配合处理后端返回认证授权验证结果数据

后端具体实现

引入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;
    }

前端具体实现

  1. 登录页向登录处理地址(该地址已在服务器配置)以form-data方式发送post请求;
  2. 封装js统一处理授权认证响应。

附录

后端项目代码

前端项目代码

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值