SpringSecurity学习记录---认证

本文详细介绍了SpringSecurity框架在Java应用中的身份验证、授权功能以及JWT令牌的使用,包括登录流程、认证代码实现、配置和UserDetailsService接口的实现。此外,还涵盖了防止CSRF攻击和跨域请求的相关内容。
摘要由CSDN通过智能技术生成

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;
    }

}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值