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