spring security的简单介绍

前言

搜索了网上的很多资料,主要参考的是这篇,推荐给大家。
https://www.cnblogs.com/xifengxiaoma/p/11106220.html

这个很早就写了,但是觉得写的不好就一直没发。今天看到有人问相关问题了,想了想,可以先放出来,后续如果要修改的话再继续。

spring security 用来做登录认证和授权的。
在这里插入图片描述

如上图,简单的讲就是 请求 经过一系列的 filter , 到达api接口, 然后返回接口处理后的结果 。

其中,绿色的部门UsernamePasswordAuthenticationFilter 和 BasicAuthenticationFilter这些是可配置是否生效的,其它的不可以控制。

配置文件概览

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    private CustomUserDetailService userDetailsService;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用自定义登录身份认证组件
        auth.authenticationProvider(jwtAuthenticationProvider());

        //user: admin  password: admin
        auth.inMemoryAuthentication()
                .withUser("admin").roles("admin").password("$2a$10$zitnDs8T9qWXn4XjstH/PuOigjid.YMWDeyuONypBLZwwBDuHWlhe");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
        http.cors().and().csrf().disable()
                //基于token,所以不需要session,也是默认配置
                //.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                // 跨域预检请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 登录URL
                .antMatchers("/signIn").permitAll()
                .antMatchers("/signUp").permitAll()
                // swagger
                .antMatchers("/swagger**/**").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/v3/**").permitAll()
                // 其他所有请求需要身份认证
                .anyRequest().authenticated();
                //不重写登录认证的话,可以直接
                //.adn().formLogin();
                //同理,开启中间的filter
                //.and().httpBasic();
                
        // 开启登录认证流程过滤器
        http.addFilterBefore(jwtLoginFilter(), UsernamePasswordAuthenticationFilter.class);
        // 访问控制时登录状态检查过滤器
        //http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
        // 退出登录处理器
        http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());

    }

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

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

    @Bean
    JwtLoginFilter jwtLoginFilter() throws Exception {
        JwtLoginFilter jwtLoginFilter = new JwtLoginFilter();
        jwtLoginFilter.setAuthenticationManager(authenticationManager());
        return jwtLoginFilter;
    }

    @Bean
    JwtAuthenticationProvider jwtAuthenticationProvider() throws Exception {
        JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider();
        jwtAuthenticationProvider.setUserDetailsService(userDetailsService);
        jwtAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return jwtAuthenticationProvider;
    }

    @Bean
    JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
        return jwtAuthenticationFilter;
    }
}

登录认证有两种处理方式,一种是采用拦截登录接口的方式,
另一种是 在controller里面直接认证,然后将认证结果放入SecurityContext。

先讲下第一种方式:
理清下流程:filter 拦截接口 --> provider 验证信息是否正确 (调用retrieveUser方法去获取存储的信息) --> 返回校验结果。

UsernamePasswordAuthenticationFilter 是第一步的默认实现,拦截 /login 接口,获取name,password参数并封装成AuthenticationToken去进行下一步校验的。
如果使用默认的login接口,可以直接继承UsernamePasswordAuthenticationFilter类,并重写里面的attemptAuthentication方法。
如果使用其他接口,需要继承它的父类AbstractAuthenticationProcessingFilter类,照着写就可以的。

一. 新建JwtLoginFilter类

public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(JwtLoginFilter.class);

    public JwtLoginFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = request.getParameter("username");
            String password = request.getParameter("password");
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            //这里是继承的UsernamePasswordAuthenticationToken
            JwtAuthenticationToken authRequest = new JwtAuthenticationToken(username, password);

            authRequest.setDetails(authenticationDetailsSource.buildDetails(request));

            return getAuthenticationManager().authenticate(authRequest);
        }

    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Authentication request failed: " + failed.toString(), failed);
        }

        getRememberMeServices().loginFail(request, response);

        CustomizeResponse customizeResponse = new CustomizeResponse();
        customizeResponse.setCode(ResponseConstants.CODE_401);
        customizeResponse.setMessage(ResponseConstants.MSG_401);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(customizeResponse));
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        // 存储登录认证信息到上下文
        SecurityContextHolder.getContext().setAuthentication(authResult);
        // 记住我服务
        getRememberMeServices().loginSuccess(request, response, authResult);
        // 触发事件监听器
        if (eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        //生成token,返回
        String jwtToken = JwtTokenUtils.generateToken(authResult);

        CustomizeResponse customizeResponse = new CustomizeResponse();
        customizeResponse.setCode(ResponseConstants.CODE_200);
        customizeResponse.setMessage(ResponseConstants.MSG_200);
        customizeResponse.setData(jwtToken);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(customizeResponse));
    }


    /**
     * json形式的post请求,需要从流中读取参数 。获取请求Body
     * @param request
     * @return
     */
    public String getBody(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = request.getInputStream();
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
            String line = "";
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
}
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {

    private static final long serialVersionUID = 7303024501909676990L;

    private String token;

    public JwtAuthenticationToken(Object principal, Object credentials){
        super(principal, credentials);
    }

    public JwtAuthenticationToken(Object principal, Object credentials, String token){
        super(principal, credentials);
        this.token = token;
    }

    public JwtAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
        super(principal, credentials, authorities);
        this.token = token;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
}

第一步其实就只是通过filter拦截接口,获取参数并封装,然后调用后面的认证方法。
第二步是比较复杂的,可以看到第一步attemptAuthentication方法最后调用了authenticate方法。每个provider通过supports来表明自己是支持哪种校验的。

DaoAuthenticationProvider 是第二步的默认实现,我们可以点进它继承的AbstractUserDetailsAuthenticationProvider进去查看 authenticate 方法的具体流程,同时也可以在父类里搜到它的supports,很明显是支持我们第一步封装的JwtAuthenticationToken的。

    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

我们这里直接继承DaoAuthenticationProvider去进行重写,不继承父类了。差别在于,父类只提供了抽象方法 retrieveUser, 而子类有实现,是调用UserDetailsService().loadUserByUsername() 方法,这个我们放到第三步去介绍。

在认证方法 authenticate 里面,流程是

  1. 获取user (这里包括通过retrieveUser去获取用户信息)
  2. 2.1 preAuthenticationChecks.check() 默认实现是检查账号是否有效,过期,锁定
    2.2 additionalAuthenticationChecks 这里进行密码检查 这两步如果报错会重试一次
  3. postAuthenticationChecks.check 这里默认实现是检查密码是否过期。

二. 新建JwtAuthenticationProvider类 这里我就只重写了additionalAuthenticationChecks方法

public class JwtAuthenticationProvider extends DaoAuthenticationProvider {

    private static final Logger LOGGER = LoggerFactory.getLogger(JwtLoginFilter.class);

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            LOGGER.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(messages.getMessage("additionalAuthenticationChecks.badCredentials", "Bad credentials"));
        }

        // 验证密码是否匹配
        String password = authentication.getCredentials().toString();
        //再次加密后与数据库的进行比对
        password = SM4Util.encryptCbcDataTimes(password, null, null);
        if(!password.equals(userDetails.getPassword())){
            LOGGER.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException(messages.getMessage("additionalAuthenticationChecks.badCredentials", "Bad credentials"));
        }
    }
}

ps: 获取用户信息,即实现loadUserByUsername方法,往上翻到配置项的代码处,我们在注解bean,jwtAuthenticationProvider类的时候,向里面注入了一个我们自定义的userDetailsService,示例代码如下:

@Service
public class CustomUserDetailService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private UserRoleMapper userRoleMapper;

    @Override
    public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {
        UserDAO userDAO = userMapper.queryUserByLoginName(loginName);
        if(null == userDAO){
            throw new UsernameNotFoundException("User " + loginName + " was not found");
        }

        // 2. 设置角色
        List<String> roles = userRoleMapper.queryUserRole(userDAO.getUserId());
        Collection<GrantedAuthority> grantedAuthorities = AuthorityUtils.createAuthorityList(roles.toArray(new String[0]));
        //Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        //GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("admin");
        //grantedAuthorities.add(grantedAuthority);
        return new User(userDAO.getUserName(), userDAO.getPassword(), grantedAuthorities);
    }
}

第三步,返回结果。这个也是在前面第一步filter里面重写了的。成功或失败分别进行一些处理并放回。

这样第一种方式,拦截请求就结束了。第二种直接调请求接口的,思想同这个是一样的,就不赘述了。至于登录以外的其它请求,则可以新建filter,获取request里面的携带的之前登录认证返回的token,后续进行相应处理即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值