Spring Security是一个开源的Java框架,用于实现身份验证和授权功能。它最初是由Ben Alex开发的Acegi Security项目,在2008年与Spring框架合并成为Spring Security。
Spring Security的出现是为了解决企业级应用程序中的安全性问题。在过去,开发人员需要编写大量的安全代码来处理认证和授权,并且很容易出现漏洞。Spring Security的目标是通过提供一套简单易用、灵活可扩展的安全功能来简化开发人员的工作。
Spring Security提供了多种认证方式,包括基于表单的认证、基于HTTP Basic和Digest的认证、基于OpenID的认证等。它还支持细粒度的授权控制,可以通过配置角色、权限和访问控制表达式来限制用户的访问权限。
除了认证和授权功能外,Spring Security还提供了其他安全特性,如防止跨站点请求伪造(CSRF)攻击、防止会话固定攻击、保护敏感数据等。
一、登录流程
前端发起登录请求-->携带用户名和密码到服务器-->服务器根据用户名查询数据库信息,与前端发来的密码进行比对==》密码正确生成jwt令牌,并将用户信息存入jwt令牌中,将jwt令牌存储到redis中表示一登陆-->返回给前端
前端发起请求访问其他接口,携带token,token内容为jwt令牌的值-->后端接受到前端请求,通过Jwt过滤器解析token,并从redis读取用户信息验证是否登录过期,-->未过期则放行此次请求,并刷新token有效期。
二、SpringSecurity认证基本流程
-
UsernamePasswordAuthenticationFilter:用户名密码认证过滤器。
负责处理在登陆页面填写了用户名密码后的登陆请求。
- Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
- AuthenticationManager接口:定义了认证Authentication的方法
- UserDetailsService接口:加载用户数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。默认的实现类就是和上图一样在内存中查找,实际开发我们应自己定义实现类,覆盖默认的方式,把从内存中查询改为从数据库中查询。
- UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。一般自定义LoginUser继承UserDetails。
三、SpringSecurity认证代码实现
前端请求(username,password)--> /login 调用LoginService.login(username,password,code) (通过new UsernamePasswordAuthenticationToken(username,password)生成Authentication对象,调用AuthenticationManager.authenticate(authenticate)会进如到UserDetailService的实现类的loadUserByUsername--> UserDetailService 在loadUserByUsername(username)中查询数据库中用户信息封装到LoginUser中,调用new BCryptPasswordEncoder().matches(rawPassword,encodedPassword);进行密码验证,成功后返回UserDetails对象-->成功后回到LoginService.login()方法中,AuthenticationManager.authenticate(authenticate)执行成功后会返回Authentication对象,通过authentication.getPrincipal()得到LoginUser对象,调用TokenService.createToken(loginUser)生成令牌返回给前端。同时在TokenService.createToken会调用refreshToken刷新token时间(初次调用时会将用户信息存入到redis中)。
1.SpringSecurity配置
对于登录、注册、验证码等请求应该直接放行。
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private final CorsFilter corsFilter;
private final LogoutSuccessHandler logoutSuccessHandler;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//禁用CSRF,因为不使用session
http.csrf().disable()
//基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//过滤请求
.authorizeRequests()
//对于登录login,注册,验证码允许匿名访问
.antMatchers("/login","/register","/captchaImage").permitAll()
.antMatchers("/login","/captcha/get","/captcha/check").permitAll()
//静态资源,可匿名访问
.antMatchers(HttpMethod.GET,"/","/*.html","/**/*.html","/**/*.css","/**/*.js","/profile/**").permitAll()
// .antMatchers("/doc.html","/doc.html#/**","/doc.html/**").permitAll()
.antMatchers(
//下面是knief4j和swagger放行的内容,包含了好几个版本的knief4j,所以直接全部复制就行了
"/v3/api-docs"
, "/api/**"
,"/doc.html"
, "/webjars/**"
, "/img.icons/**"
, "/swagger-resources/**"
, "/**"
, "/v2/api-docs"
).permitAll()
//除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable()
;
http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
//jwt过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//跨域
http.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
http.addFilterBefore(corsFilter, LogoutFilter.class);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
//配置自定义UserDetialService和密码加密方式
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
2.实现UserDetailService接口
@Slf4j
@RequiredArgsConstructor
@Service
public class UserDetailServiceImpl implements UserDetailsService {
private final SysUserService userService;
private final SysPasswordService passwordService;
private final SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userService.selectSysUserByUserName(username);
if (Objects.isNull(user)){
log.info("登录用户:{} 不存在",username);
throw new ServiceException(MessageUtils.message("user.not.exists"));
} else if (UserStatus.DELETE.getInfo().equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除",username);
throw new ServiceException(MessageUtils.message("user.deleted"));
} else if (UserStatus.DISABLE.getInfo().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用",username);
throw new ServiceException(MessageUtils.message("user.blocked"));
}
passwordService.validate(user);
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user){
return new LoginUser(user.getUserId(), user.getDeptId(), user,permissionService.getMenuPermission(user));
}
}
3.实现UserDetials接口
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private Long userId ;
private Long deptId ;
/**
* 用户唯一标识
*/
private String token;
private LocalDateTime loginTime;
private LocalDateTime expireTime;
public LoginUser(SysUser user, Set<String> permissions)
{
this.user = user;
this.permissions = permissions;
}
public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)
{
this.userId = userId;
this.deptId = deptId;
this.user = user;
this.permissions = permissions;
}
/**
* 权限列表
*/
private Set<String> permissions;
/**
* 用户信息
*/
private SysUser user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@JSONField(serialize = false)
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {
return true;
}
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JSONField(serialize = false)
@Override
public boolean isEnabled() {
return true;
}
}
4.JwtAuthenticationTokenFilter
自定义一个过滤器,这个过滤器会去获取请求头中的token
,对token
进行解析取出其中的loginUser
。通过tokenService.verifyToken(loginUser);验证用户是否登录过期,并刷新token有效期。最后封装Authentication
对象存入SecurityContextHolder
。
@Component
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
private final TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
LoginUser loginUser = tokenService.getLoginUser(request);
if (!Objects.isNull(loginUser) && Objects.isNull(SecurityUtils.getAuthentication())){
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request,response);
}
}
5./login登录接口
自定义登陆接口,让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话要生成一个jwt,放入响应中返回。
为了让用户下回请求时能通过jwt识别出的是哪个用户,需要把用户信息存入redis,可以把TokenService.getToken(LoginUser.getToken)作为key。
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody){
AjaxResult result = AjaxResult.success();
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode());
result.put(Constants.TOKEN,token);
return result;
}
6.LoginService
@Component
@RequiredArgsConstructor
public class SysLoginService {
private final TokenService tokenService;
private final CaptchaService captchaService;
private final AuthenticationManager authenticationManager;
public String login(String username,String password,String code){
CaptchaVO captchaVO = new CaptchaVO();
captchaVO.setCaptchaVerification(code);
ResponseModel responseModel = captchaService.verification(captchaVO);
if (!responseModel.isSuccess()){
throw new CaptchaException();
}
Authentication authentication = null;
try {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
//该方法会调用UserDetailServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
}catch (Exception e){
if (e instanceof BadCredentialsException)
{
throw new UserPasswordNotMatchException();
}
else
{
throw new ServiceException(e.getMessage());
}
}finally {
AuthenticationContextHolder.clearContext();
}
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
return tokenService.createToken(loginUser);
}
}
7.TokenService
@Component
@RequiredArgsConstructor
public class TokenService {
private String header = "Authorization";
private String secret = "ngkajgkaidfnsmdnjaew";
private int expireTime = 30;
private static final long MILLTS_SECOND = 1000L;
private static final Long MILLTS_MINUTE_TEN = 20* 60 * 1000l;
private final RedisCache redisCache;
public LoginUser getLoginUser(HttpServletRequest request){
String token = getToken(request);
if (StringUtils.isNotEmpty(token)){
try{
Claims claims = parseToken(token);
String uuid = (String) claims.get(CacheConstants.LOGIN_TOKEN_KEY);
String tokenKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(tokenKey);
return user;
}catch (Exception e){
}
}
return null;
}
public void setLoginUser(LoginUser user){
if (!Objects.isNull(user) && StringUtils.isNotEmpty(user.getToken())){
refreshToken(user);
}
}
public void delLoginUser(String token){
if (StringUtils.isNotEmpty(token)){
redisCache.deleteObject(getTokenKey(token));
}
}
public String createToken(LoginUser user) {
String token = UUID.randomUUID().toString();
user.setToken(token);
setUserAgent(user);
refreshToken(user);
HashMap<String, Object> claims = new HashMap<>();
claims.put(CacheConstants.LOGIN_TOKEN_KEY,token);
return createToken(claims);
}
public void verifyToken(LoginUser user){
LocalDateTime userExpireTime = user.getExpireTime();
LocalDateTime now = LocalDateTime.now();
long millis = Duration.between(userExpireTime, now).toMillis();
if (millis <= MILLTS_MINUTE_TEN){
refreshToken(user);
}
}
private void refreshToken(LoginUser user) {
user.setLoginTime(LocalDateTime.now());
user.setExpireTime(user.getLoginTime().plusSeconds(expireTime));
String tokenKey = getTokenKey(user.getToken());
redisCache.setCacheObject(tokenKey,user,expireTime, TimeUnit.MINUTES);
}
public void setUserAgent(LoginUser loginUser){
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getHttpServletRequest().getHeader("User-Agent"));
String ip = IpUtils.getIpAddr();
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOperatingSystem().getName());
}
public String createToken(Map<String,Object> claims){
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512,secret)
.compact();
}
public Claims parseToken(String token){
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
public String getUsernameFromToken(String token){
return parseToken(token).getSubject();
}
private String getToken(HttpServletRequest request){
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)){
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
private String getTokenKey(String uuid){
return CacheConstants.LOGIN_TOKEN_KEY + uuid;
}
}
8.PasswordService
@Component
@RequiredArgsConstructor
public class SysPasswordService {
private final RedisCache redisCache;
private int maxRetryCount = 5;
private int lockTime = 10;
private String getCacheKey(String userName){
return CacheConstants.PWD_ERR_CNT_KEY + userName;
}
public void validate(SysUser user){
Authentication userNamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
String username = userNamePasswordAuthenticationToken.getName();
String password = userNamePasswordAuthenticationToken.getPrincipal().toString();
Integer retryCont = redisCache.getCacheObject(getCacheKey(username));
if (retryCont == null){
retryCont = 0;
}
if (retryCont >= Integer.valueOf(maxRetryCount).intValue()){
//TODO 记录登录日志
throw new UserPasswordRetryLimitExceedMatchException(maxRetryCount,lockTime);
}
if(!matches(user,password)){
retryCont = retryCont + 1;
//TODO 记录日志
redisCache.setCacheObject(getCacheKey(username),retryCont,lockTime, TimeUnit.MINUTES);
throw new UserPasswordNotMatchException();
}else {
clearLoginRecordCache(username);
}
}
public boolean matches(SysUser user,String rawPassword){
//TODO 密码匹配
return SecurityUtils.matchesPassword(rawPassword,user.getPassword());
}
public void clearLoginRecordCache(String loginName){
if (redisCache.hasKey(getCacheKey(loginName))){
redisCache.deleteObject(getCacheKey(loginName));
}
}
}
9.SercurityUtils工具类
封装了一些常用的SecurityContextHoder的方法
public class SecurityUtils {
public static Long getUserId(){
try{
return getLoginUser().getUserId();
}catch (Exception e){
throw new ServiceException("获取用户id异常", HttpStatus.UNAUTHORIZED);
}
}
public static Long getDeptId(){
try{
return getLoginUser().getDeptId();
}catch (Exception e){
throw new ServiceException("获取部门id异常", HttpStatus.UNAUTHORIZED);
}
}
public static String getUsername(){
try{
return getLoginUser().getUsername();
}catch (Exception e){
throw new ServiceException("获取用户账号异常", HttpStatus.UNAUTHORIZED);
}
}
public static LoginUser getLoginUser(){
try{
return (LoginUser) getAuthentication().getPrincipal();
}catch (Exception e){
throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
}
}
/**
* 获取Authentication
* @return
*/
public static Authentication getAuthentication(){
return SecurityContextHolder.getContext().getAuthentication();
}
public static String encryptPassword(String password){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder.encode(password);
}
public static boolean matchesPassword(String rawPassword , String encodedPassword){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder.matches(rawPassword,encodedPassword);
}
public static boolean isAdmin(Long userId){
return userId!=null && 1L == userId;
}
}