概念
认证:即登录,authentication
授权:即允许某种操作,authorization
会话:即保持已登录状态
RBAC:Role-Based Access Control ,基于角色的访问控制
业务系统:即前台用户系统
内管系统:即后台管理系统
Spring Boot Security 应用组成
1、初始化Spring Boot 应用
2、在pom中增加依赖管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId> <!-- Spring Framework依赖管理,来自Spring IO Platform项目 -->
<version>Cairo-SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId> <!-- Spring Cloud 依赖管理 -->
<version>Greenwich.M3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
加了依赖管理后,设置依赖不需要指明版本,且不需要以下配置:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
</parent>
3、配置Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
4、从数据库中查询用户详情的实现类
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名,从数据库找出用户信息
// 参数依次为:用户名,数据库里记录的密码,可用,未过期,密码未过期,未被锁定,权限列表
// Spring Security 会 自动调用 PasswordEncoder.match() 来判断密码是否正确
return new User(username, passwordEncoder.encode("密码"), true, true,
true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_角色1,权限1"));
}
}
5、登录成功 、 登录失败、登出成功 处理类
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
// 返回一个 json
response.getWriter().write(objectMapper.writeValueAsString(authentication)); // authentication 里有权限列表
}
}
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
// 返回一个 json
response.getWriter().write(objectMapper.writeValueAsString(e)); // e 认证失败的原因
}
}
@Component
public class LogOutHandler implements LogoutSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
// 返回一个 json
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
6、Spring Security 配置类
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private LogOutHandler logOutHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 使用表单登录
.loginPage("/login.html") // 未登录时重定向到登录页面, 不指定则使用Spring security 默认提供的登录页面
.loginProcessingUrl("/api/login") // 指定 登录接口 url
.successHandler(loginSuccessHandler) // 指定登录成功处理类,不指定则重定向
.failureHandler(loginFailureHandler) // 指定登录失败处理类,不指定则重定向
.and()
.authorizeRequests() // 开始授权配置
.antMatchers("/*.html").permitAll() // 对*.html 的请求,无需权限
.antMatchers(HttpMethod.POST, "/manage/*").hasRole("manager") // 对 /manage/* 的请求,需要拥有manager角色
.antMatchers("/client/*").hasAuthority("client") // 对 /client/* 的请求,需要拥有client权限
.anyRequest().authenticated() // 针对所有请求,进行身份认证
.and()
.logout() // 开始 登出配置
.logoutUrl("/signOut") // 登出接口,默认为 /logout
.logoutSuccessUrl("/login.html") // 登出重定向到的路径,默认为loginPage
.logoutSuccessHandler(logOutHandler) // 与logoutSuccessUrl互斥
.deleteCookies("JSESSION") // 登出时 清理 cookie
.and()
.csrf() // 开始csrf配置
.disable(); // 放开csrf防御
super.configure(http);
}
@Bean
public PasswordEncoder passwordEncoder() {
// 这是Spring提供的一个密码加密器,加盐散列,并将盐拼入散列值,可用防止散列撞库
return new BCryptPasswordEncoder(); // 也可以自己实现一个 PasswordEncoder
}
@Bean
// 允许跨域配置
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
图片验证码验证
1、配置Maven依赖
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
2、认证异常类
// AuthenticationException 是抽象类,不能实例化,因此需要自定义一个 验证码异常类
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
3、验证码过滤器
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
@Autowired
private LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 只过滤登录接口
if (StringUtils.equals("/api/login", request.getRequestURI()) && StringUtils.equalsIgnoreCase("post", request.getMethod())) {
try {
validate(new ServletWebRequest(request));
}
catch (ValidateCodeException e) {
loginFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
// 这里从session中取出验证码值进行比对
throw new ValidateCodeException("验证码不匹配");
}
}
4、Spring Security配置类
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ValidateCodeFilter validateCodeFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 在验证账号 之前 验证 图片验证码
http.addFilterBefore( validateCodeFilter, UsernamePasswordAuthenticationFilter.class )
.formLogin();
super.configure(http);
}
@Bean
public PasswordEncoder passwordEncoder() {
// 这是Spring提供的一个密码加密器,加盐散列,并将盐拼入散列值,可用防止散列撞库
return new BCryptPasswordEncoder(); // 也可以自己实现一个 PasswordEncoder
}
}
短信登录
1、仿照 UsernamePasswordAuthenticationToken,定义Token类
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
2、仿照 UsernamePasswordAuthenticationFilter,定义Filter类
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParameter = "mobile"; // 字段名
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/api/mobileLogin", "POST")); // 短信登录接口
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String parameter) {
this.mobileParameter = parameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return mobileParameter;
}
}
3、仿照 DaoAuthenticationProvider 实现 Provider类
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
4、短信验证码过滤器
// 与图片验证码过滤器 类似
5、配置 Filter类 和 Provider类
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
@Autowired
private MyUserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
6、Spring Security 配置
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private ValidateCodeFilter validateCodeFilter;
@Autowired
private SmsCodeAuthenticationSecurityConfig authenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore( validateCodeFilter, UsernamePasswordAuthenticationFilter.class )
.formLogin()
.apply(authenticationSecurityConfig);
super.configure(http);
}
}
RBAC数据模型
表:用户表,角色表,资源(权限)表,用户角色关系表,角色资源关系表
资源表:存储权限控制目标,例如:菜单、按钮、URL
最佳实践
1、业务系统,一般权限控制比较简单,无需RBAC
2、内管系统,需要RBAC,并且系统中有管理RBAC数据的界面
3、资源表的值可以设置为 "对象.操作",例如 "order.delete"表示订单的删除权限,"order.delete"表示订单的删除权限,"coupon.all"表示优惠券的所有权限
4、前端 根据 RBAC 数据 隐藏 入口(菜单,按钮)
5、后端 根据 RBAC 数据表存储的 角色 与 可访问的 URL 控制访问
Spring Security 整合 RBAC
1、RBAC 权限判断 类
// 案例 1
@Component("rbacService")
public class RbacService {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
if(principal instanceof UserDetails){
// 用户名
String username = ((UserDetails)principal).getUsername();
// 根据用户名 找到 当前用户可访问的 url 列表
Set<String> urls = new HashSet<String>();
for(String url : urls){
if(antPathMatcher.match(url, request.getRequestURI())){
return true;
}
}
}
return false;
}
}
// 案例 2
// 权限的格式为 module.method,判断当前url是否包含在权限列表里
@Component("rbacService")
public class RbacService {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
// 未登录
if(authentication.getPrincipal() instanceof String){
return false;
}
Collection<? extends GrantedAuthority> authorityList = authentication.getAuthorities();
String method = request.getMethod().toLowerCase();
String path = request.getServletPath();
for (GrantedAuthority authority : authorityList) {
// authority 的格式为 module.allow
// allow取值: get、post、put、delete、all,默认为all
String[] authoritySegment = authority.getAuthority().split("\\.");
String module = authoritySegment[0];
String allow = "all";
if(authoritySegment.length > 1){
allow = authoritySegment[1];
}
String regexStart = "/admin/" + module + "/";
String regexEnd = "/admin/" + module + "$";
if (path.matches(regexStart) || path.matches(regexEnd) || module.equals("all")) {
// 权限列表里有的模块,才允许访问,且请求方法需要匹配
if (method.equals("get") || method.equals(allow) || allow.equals("all")) {
return true;
}
}
}
return false;
}
}
2、Spring Security 配置
http.authorizeRequests()
.antMatchers("/*.html").permitAll() // 对*.html 的请求,放开所有权限
// 进行权限判断,此外仍然会判断是否认真
.antMatchers("/manage").access("@rbacService.hasPermission(request, authentication)")
.anyRequest().authenticated() // anyRequest()必须放在authorizeRequests的最后,且只能有一个
super.configure(http);
网友评论