美文网首页javaWeb学习
Spring Boot 2 接口权限控制Spring Secur

Spring Boot 2 接口权限控制Spring Secur

作者: AC编程 | 来源:发表于2019-06-13 16:57 被阅读40次

    一、任务描述

    有一个测试接口AuthTestController

    @RestController
    @RequestMapping("/auth")
    public class AuthTestController {
    
        @GetMapping()
        public String greeting() {
            return "Hello,Auth!";
        }
    }
    

    现在用Postman测试一下,如图:


    AuthTestController测试结果

    任何人只要知道了这个接口的地址,那么就可以在任何时候无障碍地访问这个接口并得到期望的返回值,这显然是不安全的,所以需要给这个接口加上权限控制,访问该接口的用户必须先授权,授权后再通过授权号来访问该接口。具体实现如下

    二、添加Spring Security依赖

    compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.0.5.RELEASE'
    

    添加了Spring Security依赖后,我们再来访问该接口发现提示401,说明接口已经被保护起来了,需要授权才能正常访问


    访问接口提示401

    接下来我们就来添加提供授权的代码

    三、编写提供授权的代码

    3.1 继承WebSecurityConfigurerAdapter
    @Configuration
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        private final String DEV_ENVIRONMENT = "dev";
    
        /**
         * 运行环境:dev/prod/test
         */
        @Value("${spring.profiles.active}")
        private String active;
    
        /**
         * 密码加密及校验方式
         *
         * @return
         */
        @Bean
        public BCryptPasswordEncoder bCryptPasswordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        /**
         * Web资源权限控制
         *
         * @param web
         * @throws Exception
         */
        @Override
        public void configure(WebSecurity web) throws Exception {
            super.configure(web);
            web.ignoring().antMatchers("/config/**", "/css/**", "/fonts/**", "/img/**", "/js/**");
    
            //Ant Design登录页面,限定GET,避免和 Spring Security 的login(POST方式)冲突
            web.ignoring().antMatchers(HttpMethod.GET,"/login");
    
            //Ant Design 页面
            web.ignoring().antMatchers("/","/console", "/console/**","/static/**","/*.png","/*.js","/*.css");
    
            //swagger-ui start
            web.ignoring().antMatchers("/v2/api-docs/**");
            web.ignoring().antMatchers("/swagger.json");
            web.ignoring().antMatchers("/swagger-ui.html");
            web.ignoring().antMatchers("/swagger-resources/**");
            web.ignoring().antMatchers("/webjars/**");
            //swagger-ui end
        }
    
        /**
         * HTTP请求权限控制
         *
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //本地开发环境关闭权限控制,方便测试
            if(DEV_ENVIRONMENT.equals(active)){
                http.cors().and().csrf().disable().authorizeRequests()
                        .antMatchers("/**").permitAll()
                        .anyRequest().authenticated()
                        .and()
                        .addFilter(new JwtLoginFilter(authenticationManager()))
                        .addFilter(new JwtAuthenticationFilter(authenticationManager()));
            }else{
               http.cors().and().csrf().disable().authorizeRequests()
                        .antMatchers("/user-login/verify-account").permitAll()
                        .anyRequest().authenticated()
                        .and()
                        .addFilter(new JwtLoginFilter(authenticationManager()))
                        .addFilter(new JwtAuthenticationFilter(authenticationManager()));
            }
    
            // 禁用 SESSION、JSESSIONID
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    
    }
    

    注意:

    .antMatchers("/user-login/verify-account").permitAll() 要写在.anyRequest().authenticated()前面,不然接口权限放行会无效
    
    3.2 继承BasicAuthenticationFilter
    public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    
        public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
            super(authenticationManager);
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            String header = request.getHeader("Authorization");
    
            if (header == null || !header.startsWith(JwtUtils.getAuthorizationHeaderPrefix())) {
                chain.doFilter(request, response);
                return;
            }
    
            UsernamePasswordAuthenticationToken authenticationToken = getUsernamePasswordAuthenticationToken(header);
    
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            chain.doFilter(request, response);
        }
    
        private UsernamePasswordAuthenticationToken getUsernamePasswordAuthenticationToken(String token) {
            String user = Jwts.parser()
                    .setSigningKey("PrivateSecret") //私钥
                    .parseClaimsJws(token.replace(JwtUtils.getAuthorizationHeaderPrefix(), ""))
                    .getBody()
                    .getSubject();
    
            if (null != user) {
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
            }
    
            return null;
        }
    }
    
    3.3 继承UsernamePasswordAuthenticationFilter
    public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
    
        private AuthenticationManager authenticationManager;
    
        public JwtLoginFilter(AuthenticationManager authenticationManager) {
            this.authenticationManager = authenticationManager;
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException {
    
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
    
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            username,
                            password,
                            new ArrayList<>()
                    )
            );
        }
    
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                                FilterChain chain, Authentication authResult) {
            String token = Jwts.builder()
                    .setSubject(((User) authResult.getPrincipal()).getUsername())
                    .setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
                    .signWith(SignatureAlgorithm.HS512, "PrivateSecret") //私钥
                    .compact();
    
            returnToken(response, JwtUtils.getTokenHeader(token));
        }
    
        private void returnToken(HttpServletResponse response, String token) {
    
            JwtToken jwtToken = new JwtToken(token);
            JSONObject responseJSONObject = new JSONObject(jwtToken);
    
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter out = null;
            try {
                out = response.getWriter();
                out.append(responseJSONObject.toString());
                out.flush();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (out != null) {
                    out.close();
                }
            }
        }
    }
    
    3.4 实现接口UserDetailsService
    @Service
    public class UserDetailServiceImpl implements UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
            //TODO 从数据库取数据
            String password = "$2a$10$hoIKMK7haFkAShKNHctxceBSCigIFOkrjOh7XNDF8s0py14RNVkXW"; //admin BCryptPasswordEncoder加密后的字符串
            //String password = userServiceImp.getUserPassWord(userName);
            return new User(userName, password, getAuthority());  //emptyList()
        }
    
        private List getAuthority() {
            return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
        }
    }
    
    3.5 JwtUtils
    public class JwtUtil {
        private static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";
    
        public static String getTokenHeader(String rawToken) {
            return AUTHORIZATION_HEADER_PREFIX + rawToken;
        }
    
        public static String getAuthorizationHeaderPrefix() {
            return AUTHORIZATION_HEADER_PREFIX;
        }
    }
    
    3.6 JwtToken
    public class JwtToken implements Serializable {
    
        private String token;
    
        public JwtToken(String token) {
            this.token = token;
        }
    
        public String getToken() {
            return token;
        }
    
        public void setToken(String token) {
            this.token = token;
        }
    }
    

    四、获得密码加密字符串

    现假设账号密码都为admin,因为在上面的代码里面我们的密码加密及校验方式用的是BCryptPasswordEncoder,所以先手动获取一下admin加密后的字符串

    public class Client {
        public static void main(String[] args) {
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            String pwd = encoder.encode("admin");
            System.out.println(pwd);
        }
    }
    

    得到加密后的字符串

    $2a$10$hoIKMK7haFkAShKNHctxceBSCigIFOkrjOh7XNDF8s0py14RNVkXW
    

    将该加密后的字符串写死在UserDetailsService 实现类的loadUserByUsername方法里,正常应该是从数据通过用户名取出密码的,现为了简化测试,先手动写死。

    五、通过账号、密码获取授权

    通过POST方式,访问login接口得到授权号,注意要传参数用户名和密码


    image.png

    login方法是Spring Security内置的一个授权接口,查看源码如下


    login接口源码

    六、通过授权号访问测试接口

    将刚才的授权号拷贝出来(不用拷贝前缀Bearer)

    eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTU2MDQxNzE1OH0.5wGV19nJR2HX6fB_2GdLlb6Q8khCA-6a9tyAOJXxpuuIProTCU3keLeFTrBQrowoOu_6dUs4Uz9uznC5eXy_sA
    

    Authorization Type 选择 Bearer Token,并在Token输入框内输入刚才的授权号,发现能正常访问测试接口了,如下图:


    授权访问

    七、SecurityConfiguration具体介绍

    现在我们重点来具体介绍一下SecurityConfiguration里相关代码的作用

    7.1 Spring Security禁用session

    我们先注释以下代码

    //http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    

    我们首先传入授权号访问测试接口,测试结果是能正常访问,现在我们再来尝试一下不传人授权号来访问测试接口,看有什么反应,测试结果如下:


    不传授权号仍然能访问测试接口

    发现很神奇,不传授权号仍然能访问测试接口,到底是哪里出了问题?我们点开Postman窗口右上角的“Cookies”发现有JSESSIONID(session的一种),JSESSIONID是Spring Boot内嵌Tomcat生成的,就是这个JSESSIONID已经记录了我们上一次请求的信息,所以现在不传人授权号,仍然可以访问到测试接口

    image.png

    我们现在手动把JSESSIONID删除,再来测试看一下,发现已经提示403 访问失败,请求被Forbidden了,接口得到很好的保护,如下图:

    不传授权号接口访问失败

    我们现在用Postman测试可以手动删除JSESSIONID,那如果是其他的client(如web、android等)访问也会有这个问题,有没有什么办法让Spring Boot内嵌Tomcat不生产这个JSESSIONID?答案是:有办法。
    刚才注释的那段代码就是用来禁用 SESSION、JSESSIONID的,我们把注释的代码打开再来测试,发现Cookies里就没有生成JSESSIONID了。

    7.2 访问swagger

    我们先注释以下代码

         // web.ignoring().antMatchers("/v2/api-docs/**");
         // web.ignoring().antMatchers("/swagger.json");
        // web.ignoring().antMatchers("/swagger-ui.html");
        // web.ignoring().antMatchers("/swagger-resources/**");
        // web.ignoring().antMatchers("/webjars/**");
    

    如果接入了swagger作为接口文档,当添加Spring Security之后,发现之前能正常访问的接口文档现在访问不了了,原因是Spring Security对swagger的访问也被加上了权限控制,如下图:


    swagger访问无权限

    swagger一般只在本地开发或内部测试环境中使用,在生成环境会被关闭,所以我们期待swagger不要被权限控制(开发环境开启Swagger,生产环境关闭Swagger),刚才我们注释的那段代码就是用来配置swagger不要被权限控制,我们打开注释代码,重新启动服务器,访问swagger,如下图:

    swagger访问正常
    7.3 本地开发环境关闭权限校验

    我们期望如果是本地开发环境则关闭权限校验,因为这样方便我们通过Postman测试接口,只有测试环境和生产环境时才开启全新校验。我们可以这样实现:

    首先,我的配置文件有三套,分别对应本地开发环境、测试环境、生产环境,在application.yml里配置采用哪套配置文件,我们可以通过spring.profiles.active这个参数来取到当前的运行环境,然后设置是否开启权限校验,所以我们在SecurityConfiguration的方法configure(HttpSecurity http)里添加了一段代码,代码如下:


    配置文件
    //本地开发环境关闭权限控制,方便测试
            if("dev".equals(active)){
                http.cors().and().csrf().disable().authorizeRequests()
                        .antMatchers("/**").permitAll();
            }else{
                http.cors().and().csrf().disable().authorizeRequests()
                        .anyRequest().authenticated()
                        .and()
                        .addFilter(new JwtLoginFilter(authenticationManager()))
                        .addFilter(new JwtAuthenticationFilter(authenticationManager()));
            }
    

    相关文章

      网友评论

        本文标题:Spring Boot 2 接口权限控制Spring Secur

        本文链接:https://www.haomeiwen.com/subject/tpysfctx.html