spring Security初体验

快速开始:

导入依赖

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
            <version>2.6.3version>
        dependency>
         
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
            <version>0.9.1version>
        dependency>

导入了启动器spring security就生效了,但是显然默认的认证授权不适用生产环境,所以我们需要自己配置认证授权的过滤器

原理初探

spring security完整流程

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器
spring Security初体验_第1张图片
图中只展示了核心过滤器;
UsernamePasswordAuthenticationFilter:负责处理我们在登录页面填写了用户名密码后得登录请求;
ExceptionTranslationFilter:处理过滤器链中抛出得AccessDeniedException;
FilterSecurityInterceptor:负责权限校验得过滤器。

通过debug查看过滤器顺序spring Security初体验_第2张图片

认证流程详解

spring Security初体验_第3张图片
Authentication:他的实现类,表示当前访问系统的用户,封装了相关的用户信息。
AuthenticationManager:定义了认证Authentication的方法,认证相关的核心接口,也是发起认证的出发点
UserDetailService:加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法。
UserDetail:提供核心用户信息,通过UserDetailService根据用户名获取u哦那个胡信息要封装成UserDeail对象返回,然后将这些信息封装到Authentication对象中,然后通过SecurityContentHolder.setAuthentication()方法,将Authentication对象封装到SecurityContentHolder对象中(上图第十步)。
ProviderManager:AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个List列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider。

只保留了关键认证部分的ProviderManager源码:

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {

    // 维护一个AuthenticationProvider列表
    private List<AuthenticationProvider> providers = Collections.emptyList();
          
    public Authentication authenticate(Authentication authentication)
          throws AuthenticationException {
       Class<? extends Authentication> toTest = authentication.getClass();
       AuthenticationException lastException = null;
       Authentication result = null;

       // 依次认证
       for (AuthenticationProvider provider : getProviders()) {
          if (!provider.supports(toTest)) {
             continue;
          }
          try {
             result = provider.authenticate(authentication);

             if (result != null) {
                copyDetails(authentication, result);
                break;
             }
          }
          ...
          catch (AuthenticationException e) {
             lastException = e;
          }
       }
       // 如果有Authentication信息,则直接返回
       if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
              	 //移除密码
				((CredentialsContainer) result).eraseCredentials();
			}
             //发布登录成功事件
			eventPublisher.publishAuthenticationSuccess(result);
			return result;
	   }
	   ...
       //执行到此,说明没有认证成功,包装异常信息
       if (lastException == null) {
          lastException = new ProviderNotFoundException(messages.getMessage(
                "ProviderManager.providerNotFound",
                new Object[] { toTest.getName() },
                "No AuthenticationProvider found for {0}"));
       }
       prepareException(lastException, authentication);
       throw lastException;
    }
}

实现

登录
1.自定义登录接口
调用ProviderManager的方法进行认证,认证通过生成jwt
把用户信息存入redis中
2.自定义UserDetailsService
查询数据库获取用户信息
校验
1.定义jwt过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder中

自定义UserDetailService

实现UserDetailsService接口,重写loadUserByUsername方法,将用户信息封装搭配UserDetails对象中。

package com.zhijin.springcloud.security.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert;
import com.zhijin.springcloud.security.TokenManager;
import com.zhijin.springcloud.security.entity.LoginUser;
import com.zhijin.springcloud.security.entity.RoleBO;
import com.zhijin.springcloud.security.entity.SecurityUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class SecurityUserService implements UserDetailsService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private TokenManager tokenManager;
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //获取登录用户信息
        Object obj = redisTemplate.opsForValue().get("user:ZJGG_userInfo_" + userName);
        LoginUser loginUser = new LoginUser();
        //如果redis中没有该数据,则从数据库中获取(如果不是单点过来的,则直接使用fegin调用用户微服务,查询数据库
        if (BeanUtil.isEmpty(obj)){
            //从数据库中获取
            loginUser = gainUser();
        }else {
             loginUser = Convert.convert(LoginUser.class, obj);
        }
        Long userId = loginUser.getId();
        String token = tokenManager.createToken(userId.toString());

        //把完整用户信息存入redis作为key
        SecurityUser securityUser = new SecurityUser();
        securityUser.setCurrentUserInfo(loginUser);
        //获取权限列表
        List<String> permissionValueList = gainRoleList().stream().map(RoleBO::getRoleCode).collect(Collectors.toList());
        securityUser.setPermissionValueList(permissionValueList);
        redisTemplate.opsForValue().set("token:" + userName + "_" + token,securityUser);

        return securityUser;
    }

    /**
     * 模拟获取用户信息
     * @return
     */
    private LoginUser gainUser(){
        LoginUser loginUser = new LoginUser();
        loginUser.setId(1L);
        loginUser.setNickName("纸巾哥哥");
        loginUser.setPassword("password");
        loginUser.setSalt("");
        loginUser.setUsername("zhijingege");
        return loginUser;
    }

    /**
     * 模拟获取权限列表
     * @return
     */
    private List<RoleBO> gainRoleList(){
        List<RoleBO> roleList = new ArrayList<>();
        RoleBO role = new RoleBO();
        role.setId(1L);
        role.setRoleCode("R0001");
        role.setRoleName("角色0001");
        roleList.add(role);
        return roleList;
    }

}

自定义登录接口

在接口中我们通过AuthenticationManager的authenticate方法进行用户认证,所以需要在SecurityConfig中把AuthenticationManager注入容器中,同时需要放行该接口;

package com.zhijin.springcloud.security.service;

import com.zhijin.springcloud.security.TokenManager;
import com.zhijin.springcloud.security.entity.LoginUser;
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;

import java.util.Objects;

@Service
public class LoginService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private TokenManager tokenManager;


    public Object login(LoginUser user) {
        //AuthenticationManager 认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //判断认证是否通过
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("认证失败");
        }
        //认证通过,把完整的用户信息存入redis(略)

        //使用userName(或者userId,只要是唯一的就可以)生成token并返回给前端
        String token = tokenManager.createToken(user.getUsername());
        return token;
    }
}

除了自定义登录接口,还可以通过配置认证过滤器实现登录

package com.zhijin.springcloud.security.filter;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhijin.springcloud.security.TokenManager;
import com.zhijin.springcloud.security.entity.LoginUser;
import com.zhijin.springcloud.security.entity.SecurityUser;
import com.zhijin.springcloud.security.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;


/**
 * 认证过滤器
 */
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.authenticationManager = authenticationManager;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.setPostOnly(false);
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            //获取用户从页面输入的登录名和密码。如果是单点过来的登录信息,需要通过单点的token获取用户的登录信息处理过后再封装到user中
            LoginUser user = new ObjectMapper().readValue(req.getInputStream(), LoginUser.class);
//            这里的第一个参数是后面UserDetailService接口方法loadUserByUsername中的参数,
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 登录成功
     * @param req
     * @param res
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        SecurityUser user = (SecurityUser) auth.getPrincipal();
        String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
        redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());

        ResponseUtil.fail(200,"登录成功");
    }

    /**
     * 登录失败
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
            ResponseUtil.fail(500,"登录失败");
    }
}


token认证

package com.zhijin.springcloud.security.filter;


import com.zhijin.springcloud.security.TokenManager;
import com.zhijin.springcloud.security.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * token认证
 */
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {
        super(authManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        logger.info("================="+req.getRequestURI());
        //获取token
        String token = req.getHeader("token");
        if(org.apache.commons.lang3.StringUtils.isEmpty(token)) {
            chain.doFilter(req, res);
            return;
        }
        if(req.getRequestURI().indexOf("admin") == -1) {
            chain.doFilter(req, res);
            return;
        }
        //解析token
        UsernamePasswordAuthenticationToken authentication = null;
        try {
            authentication = getAuthentication(req);
        } catch (Exception e) {
            ResponseUtil.fail(500,e.getMessage());
        }

        if (authentication != null) {
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } else {
            ResponseUtil.fail(500,"登录失败");
        }
        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // token置于header里
        String token = request.getHeader("token");
        if (token != null && !"".equals(token.trim())) {
            String userName = tokenManager.getUserFromToken(token);

            List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            for(String permissionValue : permissionValueList) {
                if(StringUtils.isEmpty(permissionValue)) continue;
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
                authorities.add(authority);
            }

            if (!StringUtils.isEmpty(userName)) {
                return new UsernamePasswordAuthenticationToken(userName, token, authorities);
            }
            return null;
        }
        return null;
    }
}

核心配置类

package com.zhijin.springcloud.security.config;

import com.zhijin.springcloud.security.DefaultPasswordEncoder;
import com.zhijin.springcloud.security.TokenManager;
import com.zhijin.springcloud.security.UnauthorizedEntryPoint;
import com.zhijin.springcloud.security.filter.TokenAuthenticationFilter;
import com.zhijin.springcloud.security.filter.TokenLoginFilter;
import com.zhijin.springcloud.security.handler.CustomizeAuthenticationFailureHandler;
import com.zhijin.springcloud.security.handler.CustomizeAuthenticationSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;


/**
 * 核心配置类
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private TokenManager tokenManager;
    @Autowired
    private DefaultPasswordEncoder defaultPasswordEncoder;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private CustomizeAuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private CustomizeAuthenticationFailureHandler authenticationFailureHandler;
//    @Autowired
//    private CustomizeLogoutSuccessHandler logoutSuccessHandler;

    @Autowired
    public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
                                  TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.userDetailsService = userDetailsService;
        this.defaultPasswordEncoder = defaultPasswordEncoder;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }


    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置设置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.exceptionHandling()
                .authenticationEntryPoint(new UnauthorizedEntryPoint())//异常处理
                .and().formLogin().successHandler(authenticationSuccessHandler)//登录成功逻辑处理
                .failureHandler(authenticationFailureHandler)//登录失败逻辑处理
                .and().logout().permitAll()
//                .logoutSuccessHandler(logoutSuccessHandler)//登出成功逻辑处理
                .and().csrf().disable()//防止csrf攻击
                .authorizeRequests()
                .antMatchers("admin/**").hasAnyAuthority("admin")//只有指定角色才能访问admin路径下的
//               .sessionManagement().maximumSessions(1)//同一个账号只能登录一次
//                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//不通过session获取SecurityContext
//                .authorizeRequests()//授权请求
//                .anyRequest().authenticated()//需要登录
//                .anyRequest().permitAll()//所有请求通过
//                .and().logout().logoutUrl("/admin/acl/index/logout")//登出页面
//                .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()//登出处理
                .and().addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))//认证过滤
                .addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic()//授权过滤
        ;
    }

    /**
     * 密码处理
     * @param auth
     * @throws Exception
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
    }

    /**
     * 配置哪些请求不拦截
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/api/**",
                "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"
        );
    }
}


配置参数

anyRequest          |   匹配所有请求路径
access              |   SpringEl表达式结果为true时可以访问
anonymous           |   匿名可以访问
denyAll             |   用户不能访问
fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
hasRole             |   如果有参数,参数表示角色,则其角色可以访问
permitAll           |   用户可以任意访问
rememberMe          |   允许通过remember-me登录的用户访问
authenticated       |   用户登录后可访问

你可能感兴趣的:(springcloud,java,security,spring,java,spring,boot)