睁眼写BUG,闭眼改BUG。

Spring Boot (9) 安全管理

2019.07.05

在 Java 开发领域常见的 安全框架有 Shiro 和 Spring Security 。 Shiro 是一个轻量级的安全管理框架,提供了认证、授权、会话管理、密码管理、缓存管理等功能, Spring Security 是一个相对复杂的安全管理框架,功能比Shiro 更加强大,权限控制细粒度更高 ,对 OAuth 2 的 支持也 更友好,又因为 Spring Security 源自Spring 家族,因 此可以和 Spring 框架无缝整合,特别是 Spring Boot 中提供的自动化配置方案,可以让 Spring Security 的使用 更加便捷。

Spring Security

引入依赖

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

测试

编写一个控制器

/**
 *
 *
 * @Auther: MaWenyi
 * @Date: 2019/6/30
 * @Description: work.idler.dtai.controller
 * @version: 1.0
 */
@Api(value = "测试")
@RestController
public class HelloController {

    @ApiOperation(value = "hello请求", notes = "无参")
    @GetMapping("/hello")
    public String hello() {
        return "Hello";
    }
}

访问/hello接口时Spring Security就会拦截, 需要登录才能访问。

配置登录用户名和密码

  # security 配置
  security:
    user:
      name: admin
      password: xxx
      roles: admin

基于内存的配置

MyWebSecurityConfig.java

/**
 * Spring Security 配置
 *
 * @author iscolt
 * @date 2019/07/08
 */
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().ignoringAntMatchers("/druid/*");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	// 添加两个用户
        auth.inMemoryAuthentication()
                .withUser("admin").password("xxxx").roles("ADMIN","USER")
                .and()
                .withUser("iscolt").password("xxxx").roles("USER");
    }
}

HttpSecurity

权限管理

/**
 * Spring Security 配置
 *
 * @author iscolt
 * @date 2019/07/08
 */
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 不拦截druid
        http.csrf().ignoringAntMatchers("/druid/*");

        // HttpSecurity 配置
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')") // 或
                .antMatchers("/db/**").access("hasRole('DBA') and hasRole('ADMIN')") // 与
                .antMatchers("/").permitAll()
                .and()
                .formLogin()
                .loginProcessingUrl("/admin/login").permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("root").password("xxx.").roles("ADMIN", "USER", "DBA")
                .and()
                .withUser("admin").password("xxx").roles("ADMIN", "USER")
                .and()
                .withUser("user").password("xxx").roles("USER");
    }
}

说明:
> 1. 配置了三个用户
> 2. 访问admin相关需要有 ADMIN 权限
> 3. 访问user相关需要 ADMINUSER 权限
> 4. 访问db相关需要 DBAADMIN权限
> 5. 放行登录
> 6. 放行/, 除上面配置以外

登录表达详细配置(用于分离)

在前面基础上增加配置

.and()
.formLogin()
.loginPage("/admin_login")
.loginProcessingUrl("/admin/login")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(new AuthenticationSuccessHandler() {
	@Override
	public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
										HttpServletResponse httpServletResponse,
										Authentication authentication)
			throws IOException, ServletException {
		Object principal = authentication.getPrincipal();
		httpServletResponse.setContentType("application/json;charset=utf-8");
		PrintWriter out = httpServletResponse.getWriter();
		httpServletResponse.setStatus(200);
		Map<String, Object> map = new HashMap<>();
		map.put("status", 200);
		map.put("msg", principal);
		ObjectMapper objectMapper = new ObjectMapper();
		out.write((objectMapper.writeValueAsString(map)));
		out.flush();
		out.close();
	}
})
.failureHandler(new AuthenticationFailureHandler() {
	@Override
	public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
										HttpServletResponse httpServletResponse,
										AuthenticationException e)
			throws IOException, ServletException {
		httpServletResponse.setContentType("application/json;charset=utf-8");
		PrintWriter out = httpServletResponse.getWriter();
		httpServletResponse.setStatus(401);
		Map<String, Object> map = new HashMap<>();
		map.put("status", 401);
		if (e instanceof LockedException) {
			map.put("msg", "账户被锁定,登录失败!");
		} else if (e instanceof BadCredentialsException) {
			map.put("msg", "账户名或密码输入错误,登录失败!");
		} else if (e instanceof DisabledException) {
			map.put("msg", "账户被警用,登录失败!");
		} else if (e instanceof AccountExpiredException) {
			map.put("msg", "账户已过期,登录失败!");
		} else if (e instanceof CredentialsExpiredException) {
			map.put("msg", "密码已过期,登录失败!");
		} else {
			map.put("msg", "登录失败!");
		}
		ObjectMapper objectMapper = new ObjectMapper();
		out.write(objectMapper.writeValueAsString(map));
		out.flush();
		out.close();
	}
})
.permitAll();

说明:
> 1. 配置登录页面
> 2. 配置登录url
> 3. 配置用户密码属性名
> 4. 配置成功返回的json
> 5. 配置失败返回的json

postman 等工具测试

注销登录配置

在上面基础添加配置

.and()
.logout().logoutUrl("/admin/logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.addLogoutHandler(new LogoutHandler() {
	@Override
	public void logout(HttpServletRequest httpServletRequest,
					   HttpServletResponse httpServletResponse,
					   Authentication authentication) {
	}
})
.logoutSuccessHandler(new LogoutSuccessHandler() {
	@Override
	public void onLogoutSuccess(HttpServletRequest httpServletRequest,
								HttpServletResponse httpServletResponse,
								Authentication authentication)
			throws IOException, ServletException {
		httpServletResponse.sendRedirect("/admin_login");
	}
});

说明:

  1. 配置退出登录借口路径
  2. 清除session
  3. 退出成功反应

多个 HttpSecurity

/**
 * 多个HttpSecurity配置, 用于业务复杂的情况
 *
 * @author iscolt
 * @date 2019/07/11
 */
@Configuration
public class MultiHttpSecurityConfig {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        // 添加两个用户
        auth.inMemoryAuthentication()
                .withUser("root").password("mwy594211.").roles("ADMIN", "USER", "DBA")
                .and()
                .withUser("admin").password("594211").roles("ADMIN", "USER")
                .and()
                .withUser("user").password("5211").roles("USER");
    }

    @Configuration
    @Order(1)
    public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            super.configure(http);
            // TODO 进行其他配置
        }
    }

    @Configuration
    public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            super.configure(http);
            // TODO 进行其他配置
        }
    }
}

配置多个HttpSecurity时, 配置类不需要继承WebSecurityConfigurerAdapter
在配置类中创建静态内部内继承WebSecurityConfigurerAdapter即可
注意:
> 1、 @Order配置的是优先级,数字越小, 优先级越高, 未加的优先级最小

密码加密

配置文件中添加配置

    @Bean
    PasswordEncoder passwordEncoder() {
        // return NoOpPasswordEncoder.getInstance();
        /**
         * 密钥迭代次数(密码加密)
         *
         * 注册,添加用户的时候需要BCryptPasswordEncoder加密
         */
        return new BCryptPasswordEncoder(10);
    }

方法安全

在配置类上添加

/**
 * prePostEnabled = true 会解锁@PreAuthorize 和 @PostAuthorize 两个注解
 *     @PreAuthorize 在方法前验证
 *     @PostAuthorize 在方法后验证
 *     @PreAuthorize 和 @PostAuthorize 中都可以使用基于表达式的语法
 * securedEnabled = true 会解锁 @Secured 注解
 *     @Secured("ROLE_ADMIN") 表示访问该方法需要 ADMIN 权限
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)

新建TestService

/**
 * 测试业务逻辑层
 *
 * @author iscolt
 * @date 2019/07/11
 */
@Service
public class TestService {

    @Secured("ROLE_ADMIN")
    public String admin() {
        return "hello admin";
    }

    @PreAuthorize("hasRole('ADMIN') and hasRole('DBA')")
    public String dba() {
        return "hello dba";
    }

    @PreAuthorize("hasAnyRole('ADMIN', 'DBA', 'USER')")
    public String user() {
        return "You are so ordinary";
    }
}

基于数据库认证

参考文献..