• Spring Security初步学习


    Spring security --------基本介绍

      Springsecurity的核心功能主要包括

      认证:你是谁

      授权:你能搞什么

      攻击防护:防止伪造身份

      其核心就是一组过滤器链,项目启动之后就会自动配置,最核心的就是Basic Authentication Filterr 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式

      

    下面文章来源   https://www.jianshu.com/p/defa75b65a46

    基本环境的搭建立

    这里我们以springboot作为项目的基本框架,我们治理使用maven的方式来进行报管理,所以这里先给出集合Spring Security的方式

    <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>

    然后建立一个web层请求接口

    @RestController
    @RequestMapping("/user")
    public class UserController {
        @GetMapping
        public String getUsers() {       
            return "Hello Spring Security";
        }
    }

    接下来我么直接进行项目的运行,并进行接口的调用就可以看到效果了

    我们首先通过浏览器进行接口的调用,直接访问http://localhost:8080/user.如果接口能正常访问,那么应该显示“Hello Spring Security”

    但是我们是没法正常访问的,出现了下面的身份验证输入框

      

       这是因为在SpringBoot中,默认的Spring Security就是生效了的,此时的接口都是被保护的,我们需要通过验证才能进行正常的访问,Spring Security 提供了一个默认的用户,用户名是user,用户密码是启动项目的时候自动生成的,我们查看启动日志,就会在启动日志中看到这么一段log

    Using default security password: 62ccf9ca-9fbe-4993-8566-8468cc33c28c

    当然你看到的password肯定和我的不一样,我们直接用user和启动日志中的秘密进行登陆。

    登陆成功后,就能跳转到正常的页面了

    如果不想一开始的时候就使用springSecurity ,可以在配置文件中做如下配置

     security 使能
    security.basic.enabled = false

    刚才开到的登陆框是spring security框架自己提供的,被成为httpBasicLogin.显示它不是我们产品上想要的,我们前端一般通过表单提交的方式进行用户登陆验证,所以我们需要定义自及的认证逻辑。

     改造1,使用表单登陆

    前端写一个登陆页面,使用thymeleaf 模板引擎,login.html文件

    <!DOCTYPE html>
    <html id="ng-app" ng-app="app"  xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8"/>
        <title>home</title>
    </head>
    <body>
    <form  class="form-signin" action="/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">
                    <button type="submit"  class="btn btn-lg btn-primary btn-block" >登录</button>
                </td>
            </tr>
        </table>
    </form>
    </body>
    </html>

    写一个controller方法指向我们的登陆页面,不能使用@restcontroller,和 @Responsebody ,否则就返回字符串了

    @RequestMapping("/login")
        public String userLogin() {
            return "login";
        }

    还需要配置上

     定位模板的目录
    spring.thymeleaf.prefix=classpath:/templates/
    # 给返回的页面添加后缀名
    spring.thymeleaf.suffix=.html
    spring.thymeleaf.content-type=text/html
    spring.thymeleaf.mode=HTML5

    2. 添加一个SecurityConfig 继承WebSecurityConfigureAdapter

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .antMatchers("/css/**", "/index").permitAll()       
                    .antMatchers("/user/**").hasRole("USER")            
                    .and()
                .formLogin()
                    .and()
                    .csrf().disable() //关闭CSRF
                    .formLogin().loginPage("/login")
                    .loginProcessingUrl("/form")
                    .defaultSuccessUrl("/index") //成功登陆后跳转页面
                    .failureUrl("/loginError").permitAll(); 
        }
        
    }

    与/css /** 和index匹配得请求时完全可以访问的。

    /user/** 的请求的要求用户进行身份验证,并且必须与USER角色相关联。

    使用自定义登陆页面和失败URL启用基于扁担的身份验证。要登陆验证的页面,登陆后要跳转的页面,登陆失败后要跳转的页面。

    loginPage自定义登陆页面url,默认为/login

    login-procesion-rul登陆请求拦截的url,也就是form表单提交时指定的action.

    failureUrl = 表示登陆出错的页面,

    .csf().disable():Spring Security4默认是开启CSRF的,所以需要请求中包含CSRF的token信息。这里不天年这段代码的话会出现异常,加上的话可以关闭csrf

    测试:

    1、输入网址:http://127.0.0.1:8081/index,自动跳转到:http://127.0.0.1:8081/login,返回登陆页面
    2、输入账号密码:错误的话返回http://127.0.0.1:8081/loginError,登陆失败页面
    正确的话:返回http://127.0.0.1:8081/index,登陆成功页面
    改造2 :自定义用户名和密码

    很显然,这样改造之后,虽然登陆页面好看了,但还远远不能满足我们的应用需求,所以第二步我们改造自定义的用户名和密码。
    自定义用户名和密码,有两种方式,一种实在代码中写死,也就是官方中的demo,另一种是使用数据库
    首先是第一种:如:
    @Autowired
            public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
                    auth
                            .inMemoryAuthentication()
                                    .withUser("user").password("password").roles("USER");
            }

    我们也照样,这是把用户名改成admin,密码改成123456,roles是该用户的角色,我们后面在详细介绍。

    还有一种方法是重写configure(AuthenticationManagerBuider rath) 方法,这个和上面的方法作用是一样的

    @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就可以了。

    你可以多几个用户,就多几个withUser 

    接下来我们要提供自定义的用户认证机制及处理过程。

    在讲这个之前,我们需要直到Spring security 的原理,spring security  的原理就是很多的拦截器对URL进行拦截,一次来管理登陆验证和用户权限。

    用户登陆,会被AuthenticationProcessingFilter拦截,调用AuthenticationManager的实现,而且AuthenticationManager会被ProviderManager来获取用户验证信息(不同的Provider调用服务不同,因为这些信息可以实在数据库上,可以是在LDAP服务器上,可以是xm配置文件上等),如果验证通过后会将用户的权限信息封装一个User放到Spring的全局缓存SecurityContextHolder中,已备后面访问时使用。

    所以我们要自定义用户的校验机制的话,我们只需要实现自己的AuthenticationProvider就可以了。在使用AuthenticatonProvider之前,我们需要提供一个获取用户信息的服务,实现UserDetailService接口。

    用户名密码->(Authentication(未认证)-> AuthenticatonManager->AuthenticatonProvider->UserDetailService->UserDetails->Authentication(已认证))

    了解了原理之后,我们就开始写代码

    UserDetails接口

    第一步:我们自定义自己的用户信息类,UserInfo继承UserDetails接口

    public class User implements UserDetails {
       private Long id;
       private String username;
       private String password;
       private String nickname;
       private boolean enabled;
       private List<Role> roles;
       private String email;
       private String userface;
       private Timestamp regTime;
    
       @Override
       @JsonIgnore
       public boolean isAccountNonExpired() { // 帐户是否过期
           return true;
       }
    
       @Override
       @JsonIgnore
       public boolean isAccountNonLocked() { // 帐户是否被冻结
           return true;
       }
    
        // 帐户密码是否过期,一般有的密码要求性高的系统会使用到,比较每隔一段时间就要求用户重置密码
       @Override
       @JsonIgnore
       public boolean isCredentialsNonExpired() { 
           return true;
       }
    
       @Override
       public boolean isEnabled() {  // 帐号是否可用
           return enabled;
       }
    
       public void setEnabled(boolean enabled) {
           this.enabled = enabled;
       }
    
       @Override
       @JsonIgnore
       public List<GrantedAuthority> getAuthorities() {
           List<GrantedAuthority> authorities = new ArrayList<>();
           for (Role role : roles) {
               authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
           }
           return authorities;
       }
    
     //....getter setter
    }

    UserDetailService接口

    然后实现第二个类UserService来返回UserIInfo的对象实例

    @Component
    public class MyUserDetailsService implements UserDetailsService {
           
          @Override
          public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                //这里可以可以通过username(登录时输入的用户名)然后到数据库中找到对应的用户信息,并构建成我们自己的UserInfo来返回。           
                //这里可以通过数据库来查找到实际的用户信息,这里我们先模拟下,后续我们用数据库来实现
               if(username.equals("admin")) {  
                  //假设返回的用户信息如下;
                  User userInfo=new User();
                  userInfo.setUsername("admin");
                  userInfo.setPassword("123456");
                  Role role = new Role(1L,"admin");
                  List<Role> list = new ArrayList();
                  list.add(role);
                  userInfo.setRoles(list);
                  return userInfo;                             
                }           
                return null;                       
          }
    }

    到这里了为止,我们自己定义的UserIndo类和从数据库中返回的具体用户信息已经实现,接下来我们要实现的是,我们自己的AuthenticationProvider

    AuthenticationProvider接口

    新建类MyAuthticationProvider 继承AuthenticationProvider

    完整的代码如下:

    @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.getCredentials();// 这个是表单中输入的密码;
                // 这里构建来判断用户是否存在和密码是否正确
                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(password )) {
                      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 代码文件,修改如下:

      1. 注入我们自己的AuthenticationProvider

      2.修改配置方法:

    @Autowired
        private AuthenticationProvider provider;  //注入我们自己的AuthenticationProvider
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // TODO Auto-generated method stub
            auth.authenticationProvider(provider);
    
    //        auth
    //        .inMemoryAuthentication()
    //            .withUser("admin").password("123456").roles("USER")
    //            .and()
    //            .withUser("test").password("test123").roles("ADMIN");
        }

    现在重新运行程序,则需要输入用户名密码之后才可以正常登陆。

    为了方便测试,我们调整添加另外的一个控制器/whoim代码。让他返回当前登陆的用户的信息,前面说过,他是存在securitycontextHolder全局变量中,所以我们可以这样获取

    @RequestMapping("/whoim")
          public Object whoIm()
          {
                return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
          }

    改造3. 自定义登陆成功和失败的处理逻辑,

    在现在的大多数应用中,一般都是前后端分离的,所以我们登陆成功或失败都需要json格式返回,或者登陆失败后跳转到某个具体的页面。

    接下来我们来实现这种改造。

    为了实现这个功能,我们需要写两个类,分别继承SavedRequestAwareAuthenticationSucessHandler 和simmpleUrlAuthticationFailureHandler2个类,并重写其中的部分方法。

    处理登陆成功的:

    @Component("myAuthenticationSuccessHandler")
    public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{
          
          @Autowired
          private ObjectMapper objectMapper;
          @Override
          public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
                      throws IOException, ServletException {            
                //什么都不做的话,那就直接调用父类的方法
                //super.onAuthenticationSuccess(request, response, authentication);  
                
                //这里可以根据实际情况,来确定是跳转到页面或者json格式。
                //如果是返回json格式,那么我们这么写
                
                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));
                
                
                //如果是要跳转到某个页面的,比如我们的那个whoim的则
                //new DefaultRedirectStrategy().sendRedirect(request, response, "/whoim");
                
          }
    }

    登陆失败的

    @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));
                
          }

    代码完成之后,修改配置config类代码

    添加2给主机自动注入

    @Autowired
          private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
          @Autowired
          private AuthenticationFailureHandler myAuthenticationFailHander;
          
          @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().anyRequest().authenticated()                  
                      .and()
                      .csrf().disable();            
          }

    改造4. 添加权限控制

    之前的代码我们用户的权限没有加以利用,现在我们添加权限的用法

    之前的登陆验证通俗的来说,就是来判断你是谁,(认证)

    而权限控制就是来确定,你能作什么,或者你不能作什么(权限)

    在讲这个之前,我们简单说下,对于一些资源不需要权限认证的,那么就可以在Config中添加过滤条件,如:

    @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

     .antMatchers这里也可以限定HttpMethod的不同要求的不同权限,如post要求,管理员权限,get需求,user权限,同时可以通过通配符来实现。

    Spring Security的校验原理:左手配置信息,右手登陆的用户信息,中间投票器

    从我们的配置信息中获取相关的url和需要的全新啊信心,然后获取用户的用户信息,然后通过AccessDecisionManager来验证,这里面有多个投票器AccesDecisionVoter。默认有集中实现,比如1飘否决,全票否决。全票通过才算通过,只有一个通过全票通过,类似这种的。

    WebExpressionVoter是Spring Security默认的提供的web开发的投票器,默认的是只要一票通过,那么久全票通过,

    有趣的是可以从filterSecurityInterceptor这个过滤器入口,来查看整个流程。

    内嵌表达式prrmitAll denyAl等等。

    每一个权限表达式都有一个对应的一个方法,

    如果需要同时满足多个眼球,不能连写,

    如果有个url组要管理权限同时需要限定ip的话,不能.hasRole("ADMIN").hasIPAddress("192.168.1.1")

    而是需要使用acess方法.acess(hasrole("admin") and hasIpAddress("192.168..1.1"))这种。

    我们可以自己写权限表达式码,可以,稍后使用硬编码实现。

    带式在代码中写入的,这样的灵活性不高,

    改造4 添加基于RBAC (role-based-acess control)权限控制

    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;
          }
    }
    @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();            
          }

    改造5 记住我们的功能Remeber me 

    本质是通过token来读取用户信息,所以服务器端需要存储下token信息

    genuine官方文档,token可以使用数据的存段

    数据库脚本

    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
    
          /**
           * 记住我功能的token存取器配置
           * @return
           */
          @Bean
          public PersistentTokenRepository persistentTokenRepository() {
                JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
                tokenRepository.setDataSource(dataSource);
                return tokenRepository;
          }

    修改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();  

    在login上加一句

    <tr>
                <td colspan="2"><input type="checkbox" name="remember-me" value="true"/>记住我</td>
            </tr>

     然后重启服务,就可以看到上边信息

  • 相关阅读:
    第19章 网络通信----网络程序设计基础
    第18章 多线程----线程同步
    第18章 多线程----线程的优先级
    一款基于jquery和css3的响应式二级导航菜单
    一款纯css3实现的颜色渐变按钮
    一款基于jquery的手风琴显示详情
    推荐10款纯css3实现的实用按钮
    一款纯css3实现的数字统计游戏
    一款基于jquery ui的动画提交表单
    一款纯css实现的漂亮导航
  • 原文地址:https://www.cnblogs.com/dousil/p/12911545.html
Copyright © 2020-2023  润新知