欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Security 前后端分离下的权限控制

程序员文章站 2022-07-10 10:41:46
...

Spring Security 前后端分离下的权限控制

简单来说,前后端分离的项目和前后端不分离的项目区别在于:
前者是通过响应状态来驱动业务的,而后者可以直接进行页面的跳转,进而驱动业务的。

依赖

<!--SpringBoot Security模块-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>


<!-- kaptcha验证码 -->
<dependency>
  <groupId>com.github.penggle</groupId>
  <artifactId>kaptcha</artifactId>
  <version>2.3.2</version>
</dependency>

<!--redis-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>2.9.0</version>
</dependency>

这里使用redis,实现用户令牌token的免密登录。

SecurityConfiguration:springsecurity的配置类

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)  //  启用方法级别的权限认证
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    MyAuthenctiationEntryPointHandler myAuthenctiationEntryPointHandler;//未登录
    @Autowired
    MyAuthenctiationSuccessHandler myAuthenctiationSuccessHandler;//登陆成功
    @Autowired
    MyAuthenctiationFailureHandler myAuthenctiationFailureHandler;//登录失败
    @Autowired
    MyAuthenctiationDeniedHandler myAuthenctiationDeniedHandler;//无权访问
    @Autowired
    MyAuthenctiationLogoutSuccessHandler myAuthenctiationLogoutSuccessHandler;//退出成功
    @Autowired
    MyAuthenctiationInvalidSessionStrategy mMyAuthenctiationInvalidSessionStrategy;//session到期
    @Autowired
    MyAuthenctiationSessionInformationExpiredStrategy myAuthenctiationSessionStrategy;//session到期,被登陆


    //注入封装账号信息的处理bean
    @Bean
    UserDetailsService detailsService() {
        return new AuthUserDetailsService();
    }


    /*配置用户登录拦截器,解决username无法获取的问题*/
    @Bean
    LoginAuthenticationFilter loginAuthenticationFilter() throws Exception {
        LoginAuthenticationFilter filter = new LoginAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManagerBean());
        filter.setAuthenticationSuccessHandler(myAuthenctiationSuccessHandler);//登录成功
        filter.setAuthenticationFailureHandler(myAuthenctiationFailureHandler);//登录失败
        return filter;
    }




    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/attendance/userinfo/loginValidateCode","/swagger-ui.html","/swagger-resources/configuration/security","/swagger-resources/configuration/ui","/swagger*//**","/api/v1/login","/v2/api-docs", "/configuration/ui", "/swagger-resources", "/configuration/security", "/swagger-ui.html", "/webjars/**");
    }


    //用户授权操作
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("Spring Security 被启用");
        http
                .addFilterAt(loginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/attendance/userinfo/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(myAuthenctiationEntryPointHandler)//未登录402
                .accessDeniedHandler(myAuthenctiationDeniedHandler)//无权访问403
                 .and()
                .formLogin().loginPage("/attendance/userinfo/loginUser")
                .successHandler(myAuthenctiationSuccessHandler)//登陆成功200
                .failureHandler(myAuthenctiationFailureHandler)//登陆失败401
                .permitAll()
                .and()
                .sessionManagement()//session到期提示
                .invalidSessionStrategy(mMyAuthenctiationInvalidSessionStrategy)//session到期101
                .and()
                .requestCache().disable()
                .logout()
                .logoutSuccessHandler(myAuthenctiationLogoutSuccessHandler)//退出登陆200
                .and()
                .csrf().disable(); //关闭CSRF
    }


    //设置加密方式
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean
    public SecurityContextRepository securityContextRepository() {
        //设置对spring security的UserDetails进行session保存,这个必须要有,不然不会保存至session对应的缓存redis中
        HttpSessionSecurityContextRepository httpSessionSecurityContextRepository =
                new HttpSessionSecurityContextRepository();
        return httpSessionSecurityContextRepository;
    }


    //加入中间验证层,可实现自定义验证用户等信息
    @Bean
    public AuthenticationProvider authenticationProvider() {
        AuthenticationProvider provider =new MyAuthenticationProvider();
        return provider;
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }




}

LoginAuthenticationFilter:解决前段端分离项目中参数无法正常注入的问题

public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {


    @Autowired
    RedisTemplate redisTemplate;


    @Autowired
    UserService userService;




    public static LoginAuthenticationFilter loginAuthenticationFilter;


    // 关键3
    @PostConstruct
    public void init() {
        loginAuthenticationFilter = this;
        loginAuthenticationFilter.userService = this.userService;
    }


    public LoginAuthenticationFilter() {
        AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher("/attendance/userinfo/loginUser", "POST");
        this.setRequiresAuthenticationRequestMatcher(requestMatcher);
        this.setAuthenticationManager(getAuthenticationManager());
    }


    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {//必须post登录
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        String token =null;
        String username = null;
        String password = null;
        String vercode = null;
        //以json形式处理数据
        try {
            //将请求中的数据转为map
            Map<String,String> map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            username = map.get("userName");
            password = map.get("password");
            vercode = map.get("vercode");
            token= map.get("token");
            request.getSession().setAttribute("vercode", vercode);
        } catch (IOException e) {
            e.printStackTrace();
        }


        if(token ==null){//使用密码进行登录
            //如果是application/json类型,做如下处理
            if(request.getContentType() != null && (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)||request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE))){


                if (username == null) {
                    username = "";
                }
                if (password == null) {
                    password = "";
                }
                username = username.trim();
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        }else {//使用用户令牌进行登录
            String usernametemp = null;
            if(redisTemplate.opsForValue().get("token_"+token)!=null){
                usernametemp =redisTemplate.opsForValue().get("token_"+token).toString();
            }
            if(StringUtils.isBlank(usernametemp)){
                throw new UsernameNotFoundException("用户令牌过期");
            }
            request.getSession().setAttribute("token",token);
            UserInfoPojo userInfoPojo =userService.loginbyUsername(usernametemp);
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(usernametemp, userInfoPojo.getPassword());
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }

        //否则使用官方默认处理方式
        return super.attemptAuthentication(request, response);
    }
}

MyAuthenticationProvider:自定义的用户登录部分

/**
* 自定义校验-密码、图片验证码
*
*/
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
    //日志记录器
    private static Logger logger= LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);


    @Autowired
    private AuthUserDetailsService authUserDetailsService;
    @Autowired
    HttpServletRequest httpServletRequest;
    @Autowired
    PasswordEncoder passwordEncoder;


    /**
     * 登录验证码SessionKey
     */
    public static final String LOGIN_VALIDATE_CODE = "login_validate_code";


    /**
     * 自定义验证方式
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {


        String token =null;
        String password =null;
        UserDetails user =null;
        if(httpServletRequest.getSession().getAttribute("token")!=null){
            token = httpServletRequest.getSession().getAttribute("token").toString();
        }
        if(StringUtils.isBlank(token)){//使用密码进行登录  否则就是使用用户令牌进行登录
            String requestCode = httpServletRequest.getSession().getAttribute("vercode").toString();
            HttpSession session = httpServletRequest.getSession();
            String saveCode = httpServletRequest.getSession().getAttribute(LOGIN_VALIDATE_CODE).toString();
            //获取到session验证码后随时清除
            if(!StringUtils.isEmpty(saveCode)) {
                session.removeAttribute(LOGIN_VALIDATE_CODE);//captcha
                session.removeAttribute("vercode");
            }
            logger.info("requestCode:"+requestCode+",saveCode:"+saveCode);
            if(StringUtils.isEmpty(saveCode) || StringUtils.isEmpty(requestCode) || !requestCode.equals(saveCode)) {
                logger.info("图片验证码错误!");
                throw new DisabledException("图形验证码错误!");
            }
            logger.info("验证码验证成功");
            String username = authentication.getName();
             password = (String) authentication.getCredentials();
             user = authUserDetailsService.loadUserByUsername(username);
            //加密过程在这里体现
            logger.info("结果CustomUserDetailsService后,已经查询出来的数据库存储密码:" + user.getPassword());
            if (!passwordEncoder.matches(password, user.getPassword())) {
                logger.info("登录用户密码错误!");
                throw new DisabledException("登录用户密码错误!");
            }
        }else {//使用用户token进行登录
            String username = authentication.getName();
            password = (String) authentication.getCredentials();
             user = authUserDetailsService.loadUserByUsername(username);
        }
        Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
        return new UsernamePasswordAuthenticationToken(user, password, authorities);
    }


    @Override
    public boolean supports(Class<?> arg0) {
        return true;
    }
}

KaptchaConfig:验证码配置类

/**
* 验证码配置类
*/
@Component
public class KaptchaConfig {


    @Bean
    public DefaultKaptcha getDefaultKaptcha() {
        com.google.code.kaptcha.impl.DefaultKaptcha defaultKaptcha = new com.google.code.kaptcha.impl.DefaultKaptcha();
        Properties properties = new Properties();
        // 图片边框
        properties.setProperty("kaptcha.border", "no");
        // 边框颜色
        properties.setProperty("kaptcha.border.color", "black");
        //边框厚度
        properties.setProperty("kaptcha.border.thickness", "1");
        // 图片宽
        properties.setProperty("kaptcha.image.width", "200");
        // 图片高
        properties.setProperty("kaptcha.image.height", "50");
        //图片实现类
        properties.setProperty("kaptcha.producer.impl", "com.google.code.kaptcha.impl.DefaultKaptcha");
        //文本实现类
        properties.setProperty("kaptcha.textproducer.impl", "com.google.code.kaptcha.text.impl.DefaultTextCreator");
        //文本集合,验证码值从此集合中获取
        properties.setProperty("kaptcha.textproducer.char.string", "01234567890");
        //验证码长度
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        //字体
        properties.setProperty("kaptcha.textproducer.font.names", "宋体");
        //字体颜色
        properties.setProperty("kaptcha.textproducer.font.color", "black");
        //文字间隔
        properties.setProperty("kaptcha.textproducer.char.space", "5");
        //干扰实现类
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.DefaultNoise");
        //干扰颜色
        properties.setProperty("kaptcha.noise.color", "blue");
        //干扰图片样式
        properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple");
        //背景实现类
        properties.setProperty("kaptcha.background.impl", "com.google.code.kaptcha.impl.DefaultBackground");
        //背景颜色渐变,结束颜色
        properties.setProperty("kaptcha.background.clear.to", "white");
        //文字渲染器
        properties.setProperty("kaptcha.word.impl", "com.google.code.kaptcha.text.impl.DefaultWordRenderer");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

生成验证码工具类

public class CommonUtil {
/**
* 生成验证码图片
* @param request 设置session
* @param response 转成图片
* @param captchaProducer 生成图片方法类
* @param validateSessionKey session名称
* @throws Exception
*/
public static void validateCode(HttpServletRequest request, HttpServletResponse response, DefaultKaptcha captchaProducer, String validateSessionKey) throws Exception{
    // Set to expire far in the past.
    response.setDateHeader("Expires", 0);
    // Set standard HTTP/1.1 no-cache headers.
    response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
    // Set IE extended HTTP/1.1 no-cache headers (use addHeader).
    response.addHeader("Cache-Control", "post-check=0, pre-check=0");
    // Set standard HTTP/1.0 no-cache header.
    response.setHeader("Pragma", "no-cache");


    // return a jpeg
    response.setContentType("image/jpeg");


    // create the text for the image
    String capText = captchaProducer.createText();


    // store the text in the session
    request.getSession().setAttribute(validateSessionKey, capText);


    // create the image with the text
    BufferedImage bi = captchaProducer.createImage(capText);


    ServletOutputStream out = response.getOutputStream();


    // write the data out
    ImageIO.write(bi, "jpg", out);
    try {
        out.flush();
    } finally {
        out.close();
    }
}

}

Controller层验证码

@PostMapping(value = {"/loginValidateCode"})
@ApiOperation(value = "获取验证码图片")
public void loginValidateCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
    logger.info("start UserController.loginValidateCode");
    CommonUtil.validateCode(request, response, captchaProducer, LOGIN_VALIDATE_CODE);
    logger.info("end UserController.loginValidateCode");
}

验证码部分自测

Spring Security 前后端分离下的权限控制

响应结果的封装

MyAuthenctiationDeniedHandler:

/**
* 无权访问
*/
@Component
public class MyAuthenctiationDeniedHandler implements AccessDeniedHandler {
    private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);


    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        logger.info("无权访问!");


        ResponseBody responseBody = new ResponseBody();
        responseBody.setStatus("403");
        responseBody.setMsg("Need Authorities!");
        response.getWriter().write(JSON.toJSONString(responseBody));
    }
}

MyAuthenctiationEntryPointHandler

/**
* 未登录
*
*/
@Component
public class MyAuthenctiationEntryPointHandler implements AuthenticationEntryPoint{
    private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);


    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        logger.info("未登录!");


        ResponseBody responseBody = new ResponseBody();
        responseBody.setStatus("402");
        responseBody.setMsg("Need Login!");
        response.getWriter().write(JSON.toJSONString(responseBody));
    }
}

MyAuthenctiationFailureHandler

/**
* 登录失败
*
*/
@Component("myAuthenctiationFailureHandler")
public class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);


    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        logger.info("登录失败!");


        ResponseBody responseBody = new ResponseBody();
        responseBody.setStatus("401");
        responseBody.setMsg("Login Failure!");
        response.getWriter().write(JSON.toJSONString(responseBody));
    }
}

MyAuthenctiationInvalidSessionStrategy

/**
* session到期
*
*/
@Component
public class MyAuthenctiationInvalidSessionStrategy implements InvalidSessionStrategy{
    private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);


    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        logger.info("session到期!");


        ResponseBody responseBody = new ResponseBody();
        responseBody.setStatus("101");
        responseBody.setMsg("Session Expires!");
        response.getWriter().write(JSON.toJSONString(responseBody));

    }
}

MyAuthenctiationLogoutSuccessHandler

/**
* 退出登录
*
*/
@Component
public class MyAuthenctiationLogoutSuccessHandler implements LogoutSuccessHandler{


    @Autowired
    RedisTemplate redisTemplate;


    private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);


    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        logger.info("退出登录!");
        String username = authentication.getName();
        //删除用户令牌
        String token =redisTemplate.opsForValue().get("token_"+username).toString();
        redisTemplate.delete("token_"+username);
        redisTemplate.delete("token_"+token);
        request.getSession().setAttribute("token","");
        ResponseBody responseBody = new ResponseBody();
        responseBody.setStatus("200");
        responseBody.setMsg("Logout Success!");
        response.getWriter().write(JSON.toJSONString(responseBody));
    }
}

MyAuthenctiationSessionInformationExpiredStrategy

/**
* session到期,被登录
*/
@Component
public class MyAuthenctiationSessionInformationExpiredStrategy implements InvalidSessionStrategy {
    private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);


    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        logger.info("session到期,被登录!");


        ResponseBody responseBody = new ResponseBody();
        responseBody.setStatus("102");
        responseBody.setMsg("Session Expires!");
        response.getWriter().write(JSON.toJSONString(responseBody));
    }


}

MyAuthenctiationSuccessHandler

/**
* 登录成功
*
*/
@Component("myAuthenctiationSuccessHandler")
public class MyAuthenctiationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {


    @Autowired
    RedisTemplate redisTemplate;
    private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        logger.info("登录成功!");


        String token =null;
        if(request.getSession().getAttribute("token")!=null){
            token = request.getSession().getAttribute("token").toString();
        }
        if(StringUtils.isNotBlank(token)){
            //生成uuid作为用户token
            String tokentemp  = CommonUtil.getUUID();
            String username = authentication.getName();
            //将用户令牌存在redis中 生命周期为2个小时
            redisTemplate.opsForValue().set("token_"+username,tokentemp,7200, TimeUnit.SECONDS);
            redisTemplate.opsForValue().set("token_"+tokentemp,username,7200, TimeUnit.SECONDS);
            //将用户token放在cookie中
            CookieUtil.set(response, "token",tokentemp,7200);
        }
        ResponseBody responseBody = new ResponseBody();
        responseBody.setStatus("200");
        responseBody.setMsg("Login Success!");
        responseBody.setResult(token);
        response.getWriter().write(JSON.toJSONString(responseBody));
    }
}

验证

1.使用密码进行登录

Spring Security 前后端分离下的权限控制

2.使用token进行登录

Spring Security 前后端分离下的权限控制

相关标签: java spring