spring security学习与使用

1 简介

spring security 的核心功能主要包括:

认证 (你是谁)
授权 (你能干什么)
攻击防护 (防止伪造身份)

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

在spring中,使用application配置文件进行登录拦截等安全访问,需要一大推的配置编写。采用springBoot+springSecurity更简单。

2 使用

2.1 依赖

pom.xml 中的 Spring Security 依赖:

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

2.2 继承WebSecurityConfigurerAdapter 的例子详解

例1:
WebSecurityConfig类使用了@EnableWebSecurity注解 ,以启用Spring Security的Web安全支持,并提供Spring MVC集成。它还扩展了WebSecurityConfigurerAdapter,并覆盖了一些方法来设置Web安全配置的一些细节。
configure(HttpSecurity)方法定义了哪些URL路径应该被保护,哪些不应该。具体来说,“/”和“/ home”路径被配置为不需要任何身份验证。所有其他路径必须经过身份验证。
当用户成功登录时,它们将被重定向到先前请求的需要身份认证的页面。有一个由 loginPage()指定的自定义“/登录”页面,每个人都可以查看它。
对于configureGlobal(AuthenticationManagerBuilder) 方法,它将单个用户设置在内存中。该用户的用户名为“user”,密码为“password”,角色为“USER”。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
            		//定义登录页面,未登录时,访问一个需要登录之后才能访问的接口,会自动跳转到该页面
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }//对于configureGlobal(AuthenticationManagerBuilder) 方法,它将单个用户设置在内存中。
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    //在 Java 代码中配置用户名密码
        auth
            .inMemoryAuthentication()
                .withUser("user").password("password").roles("USER");
    }
}

例2

/*使用了@EnableWebSecurity注解 ,以启用Spring Security的Web安全支持,并提供Spring MVC集成。
它还扩展了WebSecurityConfigurerAdapter,并覆盖了一些方法来设置Web安全配置的一些细节。*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    @Resource
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 解决 无法直接注入 AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    /* configure(HttpSecurity)方法定义了哪些URL路径应该被保护,哪些不应该。
    具体来说,
    .antMatchers("/login").anonymous() 允许匿名访问
    .antMatchers("/getKey","/**").permitAll()中的路径路径被配置为不需要任何身份验证。
    .anyRequest().authenticated() 所有其他路径必须经过身份验证。*/
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 基于token分布式认证,所以不需要session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 配置权限
                .authorizeRequests()
                // 登录Login 验证码CaptchaImage 允许匿名访问
                .antMatchers("/login").anonymous()
                
                .antMatchers("/getKey","/**").permitAll()/*permitAll()允许所有?*/
                // 除了上面所有请求全部需要鉴权认证
                
                .anyRequest().authenticated()
                .and()
                // 允许跨域访问 等同于 config类中的corsConfigurationSource
                .cors()
                .and()
                // CRSF禁用,因为不使用session,禁用跨站csrf攻击防御,否则无法登陆成功
                .csrf().disable();

        // 退出功能
        http.logout().logoutUrl("/logout");
        // 添加JWT filter
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
//对于configure(AuthenticationManagerBuilder auth) 方法,它将单个用户设置在内存中。
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

例3:
可以在 Java 代码中配置用户名密码,首先需要我们创建一个 Spring Security 的配置类,集成自 WebSecurityConfigurerAdapter 类,如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //下面这两行配置表示在内存中配置了两个用户
        auth.inMemoryAuthentication()
                .withUser("javaboy").roles("admin").password("$2a$10$OR3VSksVAmCzc.7WeaRPR.t0wyCsIj24k0Bne8iKWV1o.V9wsP8Xe")
                .and()
                .withUser("lisi").roles("user").password("$2a$10$p1H8iWa8I4.CA.7Z8bwLjes91ZpY.rYREGHQEInNtAp4NzL6PLKxi");
    }
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

这里我们在 configure 方法中配置了两个用户,用户的密码都是加密之后的字符串(明文是 123),从 Spring5 开始,强制要求密码要加密,如果非不想加密,可以使用一个过期的 PasswordEncoder 的实例 NoOpPasswordEncoder,但是不建议这么做,毕竟不安全。

Spring Security 中提供了 BCryptPasswordEncoder 密码编码工具,可以非常方便的实现密码的加密加盐,相同明文加密出来的结果总是不同,这样就不需要用户去额外保存盐的字段了,这一点比 Shiro 要方便很多。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    VerifyCodeFilter verifyCodeFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
        http
        .authorizeRequests()//开启登录配置
        .antMatchers("/hello").hasRole("admin")//表示访问 /hello 这个接口,需要具备 admin 这个角色
        .anyRequest().authenticated()//表示剩余的其他接口,登录之后就能访问
        .and()
        .formLogin()
        //定义登录页面,未登录时,访问一个需要登录之后才能访问的接口,会自动跳转到该页面
        .loginPage("/login_p")
        //登录处理接口
        .loginProcessingUrl("/doLogin")
        //定义登录时,用户名的 key,默认为 username
        .usernameParameter("uname")
        //定义登录时,用户密码的 key,默认为 password
        .passwordParameter("passwd")
        //登录成功的处理器
        .successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write("success");
                    out.flush();
                }
            })
            .failureHandler(new AuthenticationFailureHandler() {
                @Override
                public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write("fail");
                    out.flush();
                }
            })
            .permitAll()//和表单登录相关的接口统统都直接通过
            .and()
            .logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(new LogoutSuccessHandler() {
                @Override
                public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write("logout success");
                    out.flush();
                }
            })
            .permitAll()
            .and()
            .httpBasic()
            .and()
            .csrf().disable();
    }
}

我们可以在 successHandler 方法中,配置登录成功的回调,如果是前后端分离开发的话,登录成功后返回 JSON 即可,同理,failureHandler 方法中配置登录失败的回调,logoutSuccessHandler 中则配置注销成功的回调。

2.3忽略拦截

如果某一个请求地址不需要拦截的话,有两种方式实现:

设置该地址匿名访问
直接过滤掉该地址,即该地址不走 Spring Security 过滤器链

推荐使用第二种方案,配置如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/vercode");
    }
}

2.4 jwt+springsecurity实现登录功能

在这里插入图片描述

import com.x.config.RsaKeyProperties;
import com.x.entity.User;
import com.x.service.JwtService;
import com.x.utils.DateUtil;
import com.x.utils.JwtUtil;
import com.x.utils.RedisUtil;
import com.x.utils.RsaUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

@Service
public class JwtServiceImpl implements JwtService {

    @Autowired
    private AuthenticationManager authenticationManager;
/**
 * JwtUtil 该类用于生成、验证、刷新token(签名使用RSA加密技术)
 * <p>
 * 签名:私钥加密,公钥验证 —— 保证签名不被冒充
 * 加密:公钥加密,私钥解密 —— 保证信息不被窃取
 *
 * @author
 */
    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private RsaKeyProperties rsaKeyProperties;//Rsa加密配置类
    @Autowired
    RedisUtil redisUtil;
    @Override
    public String login(String username, String password) {
        // 用户验证
        Authentication authentication = null;
        try {
            // Base64解码加密后的字符串,因为传过来的密码是经过base64加密的 RsaUtil.encrypt("123456", rsaKeyProperties.getPublicKey())
            password = RsaUtil.decrypt(password, rsaKeyProperties.getPrivateKey());
            //第一步,使用name和password封装成为的token
            //Authentication request = new UsernamePasswordAuthenticationToken(name, password);
            //第二步,将token传递给Authentication进行验证,成功认证后,返回一个Authentication实力
            //Authentication result = authenticationManager.authenticate(request);
            authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
            //SecurityContextHolder.getContext().setAuthentication(result);
        } catch (Exception e) {
            throw new RuntimeException("用户验证失败");
        }
        User user = (User) authentication.getPrincipal();
        System.out.println(user.getUserId() + "," + user.getName());

        // 生成Token
        return jwtUtil.generateToken(user, rsaKeyProperties.getPrivateKey());
    }
}

User实体类实现UserDetails:

package com.kss.entity;

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
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;

@Data
public class User implements UserDetails {
    @ExcelIgnore
    private Integer userId;
   ......
    /**
     * accountNonExpired : 账户是否没有过期
     */
    @ExcelIgnore
    private boolean accountNonExpired = true;

    /**
     * accountNonLocked : 账户是否没有被锁定
     */
    @ExcelIgnore
    private boolean accountNonLocked = true;

    /**
     * credentialsNonExpired : 密码是否没有过期
     */
    @ExcelIgnore
    private boolean credentialsNonExpired = true;

    /**
     * enabled : 账户是否可用
     */
    @ExcelIgnore
    private boolean enabled = true;

    @ExcelIgnore
    private List<Role> roles;

    /**
     * 用户拥有的角色
     *
     * @return 用户角色
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : getRoles()
        ) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

}

讲一讲在这里插入图片描述在这里插入图片描述

2.4.1 Authentication

Authentication的类图在这里插入图片描述

2.4.2UsernamePasswordAuthenticationToken对象

传入获取到的用户名和密码,而用户名对应UPAT对象中的principal属性,而密码对应credentials属性

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

//UsernamePasswordAuthenticationToken 的构造器
/**
	 * This constructor can be safely used by any code that wishes to create a
	 * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
	 * will return <code>false</code>.
	 *
	 */
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
    super(null);
    this.principal = principal;
    this.credentials = credentials;
    setAuthenticated(false);
}

2.4.3AuthenticationManager

用来处理一个认证请求。只有一个Authentication authenticate(Authentication authentication)函数。
尝试去认证传入的Authentication对象,如果认证成功,返回一个完整填充的Authentication对象(包括授予的权限)。

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
    //从Authenticaiton中提取登录的用户名。
	String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
			: authentication.getName();
    //返回登录对象
	user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
    //校验user中的各个账户状态属性是否正常
	preAuthenticationChecks.check(user);
    //密码比对
	additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
    //密码比对
	postAuthenticationChecks.check(user);
	Object principalToReturn = user;
    //表示是否强制将Authentication中的principal属性设置为字符串
	if (forcePrincipalAsString) {
		principalToReturn = user.getUsername();
	}
    //构建新的UsernamePasswordAuthenticationToken
	return createSuccessAuthentication(principalToReturn, authentication, user);
}

一个AuthenticationManager必须处理以下异常:

DisabledException:当一个账户被禁用且AuthenticationManager可以检测出来这个状态,要抛出该异常
LockedException:当一个账户被锁且AuthenticationManager可以检测这个状态,要抛出该异常
BadCredentialsException:当账户认证失败,必须抛出该异常。(一个AuthenticationManager必须检测这个状态)

这些异常应该按照顺序抛出,(比如如果一个账户被锁定,那么不进行账户认证)。

2.4.4 UserDetails

public interface UserDetails extends Serializable {
	/**
	 * 返回被授予用户的权限。
	 * @return the authorities, sorted by natural key (never <code>null</code>)
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * 返回被用来认证用户的密码。
	 * @return the password
	 */
	String getPassword();

	/**
	 * 返回被用来认证用户的用户名。
	 * @return the username (never <code>null</code>)
	 */
	String getUsername();

	/**
	 * 表明一个用户的账户是否已经过期,一个过期的用户不能被认证。
	 * @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();

	/**
	 * 表示一个用户是否被锁上。一个被锁的用户不能被认证。
	 * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
	 */
	boolean isAccountNonLocked();

	/**
	 * 表示一个用户的资格证(即密码)是否已经过期。过期的资格证明阻止认证。
	 * @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();

	/**
	 * 表示一个用户是可用还是不可用的,一个不可用的用户不能够被 认证
	 * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
	 */
	boolean isEnabled();
}

2.5 SpringSecurity如何退出登录

SpringSecurity默认为我们做了什么?
  1.使当前Session失效
  2.清除与当前用户相关的remember-me记录
  3.清空当前的SecurityContext
  4.重定向到登陆页面
在securityConfig文件中配置logout
在这里插入图片描述在这里插入图片描述

完整参考:

package cn.coreqi.security.config;

import cn.coreqi.security.Filter.SmsCodeFilter;
import cn.coreqi.security.Filter.ValidateCodeFilter;
import cn.coreqi.security.handler.CoreqiLogoutSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationSuccessHandler coreqiAuthenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler coreqiAuthenticationFailureHandler;
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(coreqiAuthenticationFailureHandler);
        SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
        //http.httpBasic()    //httpBasic登录 BasicAuthenticationFilter
        http.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)    //加载用户名密码过滤器的前面
                .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)    //加载用户名密码过滤器的前面
                .formLogin()    //表单登录 UsernamePasswordAuthenticationFilter
                    .loginPage("/coreqi-signIn.html")  //指定登录页面
                    //.loginPage("/authentication/require")
                    .loginProcessingUrl("/authentication/form") //指定表单提交的地址用于替换UsernamePasswordAuthenticationFilter默认的提交地址
                    .successHandler(coreqiAuthenticationSuccessHandler) //登录成功以后要用我们自定义的登录成功处理器,不用Spring默认的。
                    .failureHandler(coreqiAuthenticationFailureHandler) //自己体会把
                .and()
                .logout()   //退出登录相关配置
                    .logoutUrl("signOut")   //自定义退出登录页面
                    .logoutSuccessHandler(new CoreqiLogoutSuccessHandler()) //退出成功后要做的操作(如记录日志),和logoutSuccessUrl互斥
                    //.logoutSuccessUrl("/index") //退出成功后跳转的页面
                    .deleteCookies("JSESSIONID")    //退出时要删除的Cookies的名字
                .and()
                .authorizeRequests()    //对授权请求进行配置
                    .antMatchers("/coreqi-signIn.html","/code/image","/session/invalid").permitAll() //指定登录页面不需要身份认证
                    .anyRequest().authenticated()  //任何请求都需要身份认证
                    .and().csrf().disable()    //禁用CSRF
                .apply(smsCodeAuthenticationSecurityConfig);
            //FilterSecurityInterceptor 整个SpringSecurity过滤器链的最后一环
    }
}

自定义的退出成功处理器

@Slf4j
public class NRSCLogoutSuccessHandler implements LogoutSuccessHandler {
    /**
     * 退出登陆url
     *      可以在yml或properties文件里通过nrsc.security.browser.signOutUrl 进行指定
     *      我指定的默认值为"/" --- 因为如果不指定一个默认的url时,配置授权那一块会报错
     */
    private String signOutSuccessUrl;
    private ObjectMapper objectMapper = new ObjectMapper();
    public NRSCLogoutSuccessHandler(String signOutSuccessUrl) {
        this.signOutSuccessUrl = signOutSuccessUrl;
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {

        log.info("退出成功");
        //如果没有指定退出成功的页面则返回前端一个json字符串
        if (StringUtils.equalsIgnoreCase("/",signOutSuccessUrl)) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(ResultVOUtil.success("退出成功")));
        } else {
            //重定向到退出成功登陆页面
            response.sendRedirect(signOutSuccessUrl);
        }
    }
}

参考链接:

spring security——基本介绍(一):https://blog.csdn.net/qq_22172133/article/details/86503223
手把手带你入门 Spring Security!:https://www.cnblogs.com/lenve/p/11242055.html
Authentication讲解(Spring security认证):https://www.cnblogs.com/feixian-blog/p/9081261.html
SpringSecurity中的Authentication信息与登录流程:https://www.cnblogs.com/summerday152/p/13636285.html#%E7%99%BB%E5%BD%95%E6%B5%81%E7%A8%8B(感觉这个写的不错,就是看着有点困难)
SpringSecurity如何退出登录:https://www.cnblogs.com/fanqisoft/p/10659173.html
spring-security退出登陆:https://blog.csdn.net/nrsc272420199/article/details/101150634

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weightOneMillion

感谢未来的亿万富翁捧个钱场~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值