SpringSecurity 学习(一)

SpringSecurity

SpringSecurity简介
安全框架概述

什么是安全框架?解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制,非常麻烦。使用安全框架,我们可以通过配置的方式实现对资源的访问限制。

常用的安全框架
  • Spring Security:Spring家族中的一员。是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置Bean,充分利用了Spring IoC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

  • Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密和会话管理等功能。

Spring Security简介

Spring Security是一个高度自定义的安全框架。利用Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写的大量重复代码。使用Spring Security的原因很多,但大部分都是发现了JavaEE的Servlet规范或EJB规范中的安全功能缺乏典型企业应用场景。同时认识到他们在WAR和EAR级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用Spring Security解决了这些问题,也为你提供了许多其他有用的、可定制的安全功能。正如你可能知道的一个应用程序的两个主要区域是“认证”和“授权”。这两点也是SpringSecurity的核心功能。“认证”是通俗点说就是认为用户是否登录。“授权”通俗点讲就是系统判断用户是否有权限去做某些事情。

引入Spring Security 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

当我们在Spring Boot项目中引入 Spring Security 依赖后对项目不做任何配置。那么我们要访问项目的某个地址,如:/login,就会出现如下界面:
在这里插入图片描述
说明,Spring Security已经生效。这是Spring Security默认的登录逻辑生效了。输入用户user,密码为控制台打印的密码,即可登录。如果我们不想用Spring Security的默认登录逻辑,那么我们就需要自定义登录逻辑。

使用SpringSecurity 自定义登录逻辑
  • 在 Spring 的 IoC 容器中,放入一个 PassworEncoder 对象,官方推荐使用 BCryptPasswordEncoder 密码加密器。

    @Configuration
    public class SecurityConfig {
    
        @Bean
        public PasswordEncoder getPwd() {
            return new BCryptPasswordEncoder();
        }
    }
    
  • 实现 UserDetailsService 接口

  1. UserDetailsService接口的源码

    public interface UserDetailsService {
    
    	/**
    	 * Locates the user based on the username. In the actual implementation, the search
    	 * may possibly be case sensitive, or case insensitive depending on how the
    	 * implementation instance is configured. In this case, the <code>UserDetails</code>
    	 * object that comes back may have a username that is of a different case than what
    	 * was actually requested..
    	 * @param username the username identifying the user whose data is required.
    	 * @return a fully populated user record (never <code>null</code>)
    	 * @throws UsernameNotFoundException if the user could not be found or the user has no
    	 * GrantedAuthority
    	 */
    	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
    
    }
    

    该接口定义 loadUserByUsername方法,需要username参数,该参数就是前端传入的一个用户名。返回UserDetails 对象。接下来我们看一下UserDetails接口。

  2. UserDetails接口源码

    public interface UserDetails extends Serializable {
    
    	/**
    	 * Returns the authorities granted to the user. Cannot return <code>null</code>.
    	 * @return the authorities, sorted by natural key (never <code>null</code>)
    	 */
    	Collection<? extends GrantedAuthority> getAuthorities();
    
    	/**
    	 * Returns the password used to authenticate the user.
    	 * @return the password
    	 */
    	String getPassword();
    
    	/**
    	 * Returns the username used to authenticate the user. Cannot return
    	 * <code>null</code>.
    	 * @return the username (never <code>null</code>)
    	 */
    	String getUsername();
    
    	/**
    	 * Indicates whether the user's account has expired. An expired account cannot be
    	 * authenticated.
    	 * @return <code>true</code> if the user's account is valid (ie non-expired),
    	 * <code>false</code> if no longer valid (ie expired)
    	 */
    	boolean isAccountNonExpired();
    
    	/**
    	 * Indicates whether the user is locked or unlocked. A locked user cannot be
    	 * authenticated.
    	 * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
    	 */
    	boolean isAccountNonLocked();
    
    	/**
    	 * Indicates whether the user's credentials (password) has expired. Expired
    	 * credentials prevent authentication.
    	 * @return <code>true</code> if the user's credentials are valid (ie non-expired),
    	 * <code>false</code> if no longer valid (ie expired)
    	 */
    	boolean isCredentialsNonExpired();
    
    	/**
    	 * Indicates whether the user is enabled or disabled. A disabled user cannot be
    	 * authenticated.
    	 * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
    	 */
    	boolean isEnabled();
    
    }
    

    该接口中定义了:

    1. 获取权限的方法
    2. 获取用户名密码的方法
    3. 判断账户是否被锁
    4. 判断账户是否过期
    5. 判断凭证(密码)是否过期
    6. 判断账户是否可以认证

    因为UserDetails是一个接口,所以必定有实现类。我们可以发现:org.springframework.security.core.userdetails.User 类实现了UserDetails接口,并且我们也常用该对象作为 UserDetailsService 的实现类中loadUserByUsername方法的返回值。

  3. 实现UserDetailsService接口

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
        
        @Autowired
        private PasswordEncoder passwordEncoder;
        
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            if (!"admin".equals(username)){
                throw  new UsernameNotFoundException("username is not found");
            }
            String password = passwordEncoder.encode("123");
            return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList(
                    "admin,normal"
            ));
        }
    }
    

    实现该接口的loadUserByUsername方法,实现逻辑如下:

    1. 明确Username是来自前端
    2. 根据Username去查询数据库,获取到加密后的密码 和 当前用户对应的权限列表,并放回给User对象。

    为了简便,上面的代码省略了查询数据库的操作。

  • 如果不想使用SpringSecurity提供的登录页面,需要继承WebSecurityConfigurerAdapter类,重写configure(HttpSecurity http)方法。我们可以让之前的 SecurityConfig 继承WebSecurityConfigurerAdapter。

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.formLogin()
                    .loginPage("/login.html");  // 自定义的登录页面,这样就会覆盖掉SpringSecurity默认的登录页面了
        }
    
        @Bean
        public PasswordEncoder getPwd() {
            return new BCryptPasswordEncoder();
        }
    }
    

    login.html页面代码

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <form action="/login" method="post">  <!--这里的登录方式,必须是post-->
            <input type="text" name="username"/><br/>  <!-- 这里的name属性必须是username-->
            <input type="password" name="password"/><br/> <!--这里的name属性必须是password -->
            <input type="submit" value="submit">
        </form>
    </body>
    </html>
    
  1. 当我们重写了 protected void configure(HttpSecurity http) 方法之后,我们访问http://localhost:8080/login.html页面(login.html放在了项目的resource/static/目录下),就不会再去到Spring Security的默认登录页面。但是,当我们访问http://localhost:8080/main.html(main.html在项目的resource/static/目录下)这个页面的时候,也直接跳转到了该页面。这与授权逻辑相关:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login.html");
        
        http.authorizeRequests()
                .antMatchers("/login.html").permitAll()  //对login.html的请求,放行,不需要认证
                .anyRequest().authenticated();  // 其他任何请求都需要认证
    }
    

    将configure方法的逻辑修改为如上。增加除/login.html请求外,其他所有请求都要进行认证的逻辑。此时我们再访问main.html页面,就会跳转到login.html页面,先登录才行。

  2. 现在输入 http://localhost:8080/main.html页面,会跳转到自定义登录页面。
    在这里插入图片描述
    当点击提交按钮的时候,我们发现没有返回到main.html页面,还是停留在login.html页面了。我们需要重写修改configure代码:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginProcessingUrl("/login")  // 这里/login不是随便写的,要与login.html页面的form表单的action属性的值一样
                .loginPage("/login.html");
    
        http.authorizeRequests()
                .antMatchers("/login.html").permitAll()
                .anyRequest().authenticated();
    
        http.csrf().disable();  // 关闭 csrf
    
  3. 当我们输入正确的用户名密码后,我们到达了main.html页面。(我用的是Spring Boot 2.6.3 版本),其他版本有可能跳转不过去,此时我们需要修改configure代码为如下:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginProcessingUrl("/login")
                .loginPage("/login.html")
                        .successForwardUrl("/toMain");  // 这里默认发出的是Post请求, 登录成功后跳转的url,需要定义controller,重定向到main.html
    
        http.authorizeRequests()
                .antMatchers("/login.html").permitAll()
                .anyRequest().authenticated();
    
        http.csrf().disable();
    }
    

    增加 跳转的controller

    @Controller
    public class LoginController {
        @RequestMapping("toMain")
        public String toMain(){
            System.out.println("exec main method");
            return "redirect:main.html";
        }
    }
    
  4. 当我们输入的用户名密码错误时,我们会停留在login.html页面,但是url会变为http://localhost:8080/login.html?error,如果我们想自定义登录失败页面,需要这样做:

    1. 在resource/static/目录下,增加失败页面 error.html

    2. 修改protected void configure(HttpSecurity http) 代码如下:

      @Override
      protected void configure(HttpSecurity http) throws Exception {
          http.formLogin()
                  .loginProcessingUrl("/login")
                  .loginPage("/login.html")
                          .successForwardUrl("/toMain")
                          .failureForwardUrl("/toError");  // 登录失败后的跳转逻辑
      
          http.authorizeRequests()
                  .antMatchers("/error.html").permitAll() // 这里要放行/error.html请求,否则也需要登录,那么就会重新回到login.html页面了
                  .antMatchers("/login.html").permitAll()
                  .anyRequest().authenticated();
      
          http.csrf().disable();
      }
      

      增加controller代码

      @RequestMapping("toError")
      public String toError(){
          System.out.println("exec error method");
          return "redirect:error.html";
      }
      
  5. 前面我们的login.html页面,用户名,密码输入框的name属性值必须是username和password,并且登录方式必须是post。为什么呢?原因是:当我们登录时,执行我们自定义登录逻辑时,需要先执行SpringSecurity的 UsernamePasswordAuthenticationFilter g过滤器。查看过滤器的源码,我们发现,源代码里是这样规定的:

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";  // 需要前端表单的用户名为username
    
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";  // 需要前端表单的密码为password
    
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
    		"POST");
    
    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    
    private boolean postOnly = true;
    
    public UsernamePasswordAuthenticationFilter() {
    	super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
    		throws AuthenticationException {
    	if (this.postOnly && !request.getMethod().equals("POST")) {
    		throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    	}
    	String username = obtainUsername(request);
    	username = (username != null) ? username : "";
    	username = username.trim();
    	String password = obtainPassword(request);
    	password = (password != null) ? password : "";
    	UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
    	// Allow subclasses to set the "details" property
    	setDetails(request, authRequest);
    	return this.getAuthenticationManager().authenticate(authRequest);
    }
    
  6. 如果我们想修改默认的用户名和密码参数,可以修改configure类,如下:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .usernameParameter("username123")  // 自定义用户名参数
                .passwordParameter("password123")  // 自定义密码参数
                .loginProcessingUrl("/login")
                .loginPage("/login.html")
                        .successForwardUrl("/toMain")
                        .failureForwardUrl("/toError");
    
        http.authorizeRequests()
                .antMatchers("/error.html").permitAll()
                .antMatchers("/login.html").permitAll()
                .anyRequest().authenticated();
    
        http.csrf().disable();
    }
    

    并且修改登录页面,如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <form action="/login" method="post">  <!--这里的登录方式,必须是post-->
            <input type="text" name="username123"/><br/>  <!-- 这里name属性使用自定义的username123-->
            <input type="password" name="password123"/><br/> <!--这里的name属性使用自定义的password123 -->
            <input type="submit" value="submit">
        </form>
    </body>
    </html>
    
  7. 在前后端分离的项目中,我们不会使用 .successForwardUrl("/toMain") 和 .failureForwardUrl("/toError")并自定义controller的方式来实现跳转的,这种方式适用于项目内部。如果想实现项目外部的一个跳转,怎么办呢?
    通过successForwardUrl的源码,我们发现,ForwardAuthenticationSuccessHandler类,该类实现了AuthenticationSuccessHandler接口,实现内部的请求转发的。我们要实现一个外部的请求转发,显然需要实现AuthenticationSuccessHandler接口。

    public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
        private String url;
    
        public MyAuthenticationSuccessHandler(String url) {
            this.url = url;
        }
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            User user = (User)authentication.getPrincipal(); //
       		System.out.println(user.getUsername()); // 用户名
        	System.out.println(user.getPassword()); // 密码,考虑到安全的原因,这里的密码获取到的是null
        	System.out.println(user.getAuthorities()); // 该用户的权限信息
            response.sendRedirect(url);
        }
    }
    

    修改configure方法,使用我们自定义的MyAuthenticationSuccessHandler。

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.formLogin()
                    .usernameParameter("username123")
                    .passwordParameter("password123")
                    .loginProcessingUrl("/login")
                    .loginPage("/login.html")
                            //.successForwardUrl("/toMain")  这行代码与successHandler不能共存,因此注释掉
                    .successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com")) // 使用自定义的请求转换器
                            .failureForwardUrl("/toError");
    
            http.authorizeRequests()
                    .antMatchers("/error.html").permitAll()
                    .antMatchers("/login.html").permitAll()
                    .anyRequest().authenticated();
    
            http.csrf().disable();
        }
    

    同理:我们可以自定义登录失败的处理,实现登录失败后的逻辑处理。这样我们原理处理跳转成功或失败的controller代码便不再使用了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值