2.Spring Security详细使用

news2024/9/22 2:17:58

目录


1. Spring Security详细介绍

2. Spring Security详细使用

3. Spring Security实现JWT token验证

4. JWT(JSON Web Token,JSON令牌)

5. Spring Security安全注解




认证流程

1.集中式认证流程

(1)用户认证
使用UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法实现认证功能,该过滤器父类中successfulAuthentication方法实现认证成功后的操作

(2)身份校验
使用BasicAuthenticationFilter过滤器中doFilterInternal方法验证是否登录,以决定能否进入后续过滤器。

2.分布式认证流程

(1)用户认证
由于分布式项目,多数是前后端分离的架构设计,要满足可以接受异步post的认证请求参数,需要修改UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法,让其能够接收请求体。另外,默认successfulAuthentication方法在认证通过后,是把用户信息直接放入session就完事了,现在我们需要修改这个方法,在认证通过后生成token并返回给用户。

(2)身份校验
原来BasicAuthenticationFilter过滤器中doFilterInternal()方法校验用户是否登录,就是看session中是否有用户信息。修改为,验证用户携带的token是否合法,并解析出用户信息,交给Spring Security,以便于后续的授权功能可以正常使用。


基本使用Spring Security

1.引入Spring Security的依赖包

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

①:这个时候不在配置文件中做任何配置,随便写一个Controller

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

启动项目,会发现有这么一段日志
在这里插入图片描述

此时表示Security生效,默认对项目进行了保护,当访问该Controller中的接口,会见到如下登录界面
登录后才能访问/hello接口。默认用户名是user,而登录密码则在每次启动项目时随机生成,可以在项目启动日志中找到。
用户名:user
密码:日志中的“ab74417f-45d8-4645-825f-531919bacfcd”
输入之后,可以看到此时可以正常访问该接口

在这里插入图片描述

配置用户名和密码
如果对默认的用户名和密码不满意,可以在application.properties/yml中配置默认的用户名、密码和角色。这样项目启动后就不会随机生成密码了,而是使用配置的用户、密码,并且登录后还具有一个admin角色。

spring.security.user.name=hangge
spring.security.user.password=123
spring.security.user.roles=admin

②:添加一个Security配置类

/**
 *  WebSecurityConfigurerAdapter是Spring提供的对安全配置的适配器
 *  使用@EnableWebSecurity来开启Web安全
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 重写configure方法来满足需求
     * 此处允许Basic登录
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic()  // 允许Basic登录
                .and()
                .authorizeRequests()  // 对请求进行授权
                .anyRequest()  // 任何请求
                .authenticated();   // 都需要身份认证
    }
}

重启项目,访问/hello接口,输入用户凭证后结果一致。


2.配置Spring Security(安全信息)

@EnableWebSecurity注解:启用Web安全功能
其本身并没有什么用处,Spring Security的配置类还需实现WebSecurityConfigurer或继承WebSecurityConfigurerAdapter类(配置简单)

WebSecurityConfigurerAdapter类

通过重载该类的三个configure()方法来制定Web安全的细节

保护路径的配置方法

①configure(WebSecurity)
通过重载该方法,可配置Spring Security的Filter链

②configure(HttpSecurity)
通过重载该方法,可配置如何通过拦截器保护请求

方法描述
access(String)如果给定的SpEL表达式计算结果为true,就允许访问
anonymous()允许匿名用户访问
authenticated()允许认证过的用户访问
denyAll()无条件拒绝所有访问
fullyAuthenticated()如果用户是完整认证的话(不是通过Remember-me功能认证的),就允许访问
hasAnyAuthority(String…)如果用户具备给定权限中的某一个的话,就允许访问
hasAnyRole(String…)如果用户具备给定角色中的某一个的话,就允许访问
hasAuthority(String)如果用户具备给定权限的话,就允许访问
hasIpAddress(String)如果请求来自给定IP地址的话,就允许访问
hasRole(String)如果用户具备给定角色的话,就允许访问
not()对其他访问方法的结果求反
permitAll()无条件允许访问
rememberMe()如果用户是通过Remember-me功能认证的,就允许访问

Spring Security支持的所有SpEL表达式

安全表达式计算结果
authentication用户认证对象
denyAll结果始终为false
hasAnyRole(list of roles)如果用户被授权指定的任意权限,结果为true
hasRole(role)如果用户被授予了指定的权限,结果 为true
hasIpAddress(IP Adress)用户地址
isAnonymous()是否为匿名用户
isAuthenticated()不是匿名用户
isFullyAuthenticated不是匿名也不是remember-me认证
isRemberMe()remember-me认证
permitAll始终true
principal用户主要信息对象

③configure(AuthenticationManagerBuilder)
通过重载该方法,可配置user-detail(用户详细信息)服务

配置用户详细信息的方法

方法描述
accountExpired(boolean)定义账号是否已经过期
accountLocked(boolean)定义账号是否已经锁定
and()用来连接配置
authorities(GrantedAuthority…)授予某个用户一项或多项权限
authorities(List)授予某个用户一项或多项权限
authorities(String…)授予某个用户一项或多项权限
credentialsExpired(boolean)定义凭证是否已经过期
disabled(boolean)定义账号是否已被禁用
password(String)定义用户的密码
roles(String…)授予某个用户一项或多项角色

用户信息存储方式(三种)
(1)使用基于内存的用户存储
通过inMemoryAuthentication()方法,可以启用、配置并任意填充基于内存的用户存储。并且,可以调用withUser()方法为内存用户存储添加新的用户,这个方法的参数是username。withUser()方法返回的是UserDetailsManagerConfigurer.UserDetailsBuilder,这个对象提供了多个进一步配置用户的方法,包括设置用户密码的password()方法以及为给定用户授予一个或多个角色权限的roles()方法。需要注意的是,roles()方法是authorities()方法的简写形式。roles()方法所给定的值都会添加一个ROLE_前缀,并将其作为权限授予给用户。因此上诉代码用户具有的权限为:ROLE_USER,ROLE_ADMIN。而借助passwordEncoder()方法来指定一个密码转码器(encoder),可以对用户密码进行加密存储

(2)基于数据库表进行认证
用户数据通常会存储在关系型数据库中,并通过JDBC进行访问。为了配置Spring Security使用以JDBC为支撑的用户存储,可以使用jdbcAuthentication()方法,并配置他的DataSource,这样的话,就能访问关系型数据库

(3)基于LDAP进行认证
为了让Spring Security使用基于LDAP的认证,可以使用ldapAuthentication()方法


(1)内存用户

/**
 * WebSecurityConfigurerAdapter是Spring提供的对安全配置对适配器
 */
@Configuration
@EnableWebSecurity  // 使用@EnableWebSecurity来开启Web安全
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	// 配置用户及其对应的角色
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.inMemoryAuthentication()
		 .withUser("root").password("123").roles("ADMIN", "DBA")
		 .and()
		 .withUser("admin").password("123").roles("ADMIN", "USER")
		 .and()
		 .withUser("test").password("123").roles("USER");
	}
	
	/**
	* 配置URL访问权限。重写configure()来满足需求
	* @param http
	* @throws Exception
	*/
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()  // 开启HttpSecurity配置
		.antMatchers("/admin/**").hasRole("ADMIN")  // admin/**模式URL必须具备ADMIN角色
		.antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')")  // 该模式需要ADMIN或USER角色
		.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")  // 需ADMIN和DBA角色
		.anyRequest().authenticated()  // 用户访问其它URL都必须认证后访问(登录后访问)
		.and()
		.formLogin() // 允许表单登录
		.loginPage("/login.html")  // 设置表单登录页
		.loginProcessingUrl("/login")  // 使用/login的url来处理表单登录请求
		.and()
		.authorizeRequests()  // 对请求进行授权
		.antMatchers("/index.html").permitAll()  // 对index.html页面放行
		.anyRequest()  // 任何请求
		.authenticated()  // 都需要身份认证
		.and()
		.csrf().disable();  // 关闭跨站请求伪造防护
	}
	
	/**  
	* 指定密码的加密方式
	* 添加一个加密工具对bean,PasswordEncoder为接口
	* BCryptPasswordEncoder为实现类,也可以用其他加密实现类代替。如MD5等
	* @return
	*/
	@Bean
	public PasswordEncoder bCryptPasswordEncoder() {
		// 方式①:
		return new BCryptPasswordEncoder();
		
		// 方式②:
		return new PasswordEncoder() {
			@Override
			public String encode(CharSequence charSequence) {
				return charSequence.toString();
			}
			@Override
			public boolean matches(CharSequence charSequence, String s) {
				return Objects.equals(charSequence.toString(), s);
			}
		};
		
	}
}

HttpSecurity常用方法

方法描述
openidLogin()用于基于Open Id的验证
headers()将安全标头添加到响应
cors()配置跨域资源共享(CORS)
sessionManagement()允许配置会话管理
portMapper()允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
jee()配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
x509()配置基于x509的认证
rememberMe()允许配置“记住我”的验证
authorizeRequests()允许基于使用HttpServletRequest限制访问
requestCache()允许配置请求缓存
exceptionHandling()允许配置错误处理
securityContext()在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用
servletApi()将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用
csrf()添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
logout()添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”
anonymous()允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
formLogin()指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
oauth2Login()根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
requiresChannel()配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
httpBasic()配置Http Basic验证
addFilterAt()在指定的Filter类的位置添加过滤器

(2)数据库来配置用户与角色

不配置内存用户,将UserService配置到AuthenticationManagerBuilder中

通过数据库来配置用户与角色,但认证规则仍然是使用HttpSecurity进行配置,还是不够灵活;无法实现资源和角色之间的动态调整

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private UserService userService;

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

    // 配置用户及其对应的角色
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    // 配置基于内存的URL访问权限
    @Override
    protected  void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()  // 开启HttpSecurity配置
                .antMatchers("/admin/**").hasRole("ADMIN")  // admin/**模式URL必须具备ADMIN角色
                .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')")  // 该模式需要ADMIN或USER角色
                .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")  // 需ADMIN和DBA角色
                .anyRequest().authenticated()  // 用户访问其它URL都必须认证后访问(登录后访问)
                .and().formLogin().loginProcessingUrl("/login").permitAll()  // 开启表单登录并配置登录接口
                .and().csrf().disable();  // 关闭csrf
    }
    
}

(3)实现动态配置URL权限(自定义权限配置)

要实现动态配置权限,首先需要自定义FilterInvocationSecurityMetadataSource:自定义FilterInvocationSecurityMetadataSource主要实现该接口中的getAttributes()方法,该方法用来确定一个请求需要哪些角色

基于数据库resource表和role_resource表的URL权限规则配置

①首先创建resourceMapper接口:获取所有的资源
@Mapper
public interface ResourceMapperDao {
    // 获取所有的资源
    public List<Resources> getAllResources();
}
<select id="getAllResources" resultMap="ResourcesMap">
SELECT
         r.*,
         re.id AS roleId,
         re.`name`,
         re.description
        FROM resources AS r
        LEFT JOIN role_resource AS rr  ON r.id = rr.resource_id
        LEFT JOIN role AS re ON re.id = rr.role_id
</select>
②自定义实现FilterInvocationSecurityMetadataSource接口:确定请求需要角色

注意:自定义FilterInvocationSecurityMetadataSource主要实现该接口中的getAttributes()方法,该方法用来确定一个请求需要哪些角色

@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    // 创建一个AntPathMatcher,主要用来实现ant风格的URL匹配
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Autowired
    private ResourceMapperDao resourceMapperDao;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        // 从参数中提取出当前请求的URL
        String requestUrl = ((FilterInvocation) object).getRequestUrl();

        // 从数据库中获取所有的资源信息,即本案例中的Resources表以及Resources所对应的role
        // 在真实项目环境中,开发者可以将资源信息缓存在Redis或者其他缓存数据库中
        List<Resources> allResources = resourceMapperDao.getAllResources();
        // 遍历资源信息,遍历过程中获取当前请求的URL所需要的角色信息并返回
        for (Resources resource : allResources) {
            if (antPathMatcher.match(resource.getPattern(), requestUrl)) {
                List<Role> roles = resource.getRoles();
                if(!CollectionUtils.isEmpty(roles)){
                    List<ConfigAttribute> allRoleNames = roles.stream()
                            .map(role -> new SecurityConfig(role.getName().trim()))
                            .collect(Collectors.toList());
                    return allRoleNames;
                }
            }
        }
        // 如果当前请求的URL在资源表中不存在相应的模式,就假设该请求登录后即可访问,即直接返回 ROLE_LOGIN
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    // 该方法用来返回所有定义好的权限资源,Spring Security在启动时会校验相关配置是否正确
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        // 如果不需要校验,那么该方法直接返回null即可
        return null;
    }
    
    // supports方法返回类对象是否支持校验
    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}
③自定义实现AccessDecisionManager接口:角色对比

当一个请求走完FilterInvocationSecurityMetadataSource中的getAttributes()方法后,接下来就会来到AccessDecisionManager类中进行角色信息的对比

@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
    // 该方法判断当前登录的用户是否具备当前请求URL所需要的角色信息
    @Override
    public void decide(Authentication auth, Object object, Collection<ConfigAttribute> ConfigAttributes) {
        Collection<? extends GrantedAuthority> userHasAuthentications = auth.getAuthorities();
        // 如果具备权限,则不做任何事情即可
        for (ConfigAttribute configAttribute : ConfigAttributes) {
            // 如果需要的角色是ROLE_LOGIN,说明当前请求的URL用户登录后即可访问
            // 如果auth是UsernamePasswordAuthenticationToken的实例,说明当前用户已登录,该方法到此结束
            if ("ROLE_LOGIN".equals(configAttribute.getAttribute())
                    && auth instanceof UsernamePasswordAuthenticationToken) {
                return;
            }
            // 否则进入正常的判断流程
            for (GrantedAuthority authority : userHasAuthentications) {
                // 如果当前用户具备当前请求需要的角色,那么方法结束
                if (configAttribute.getAttribute().equals(authority.getAuthority())) {
                    return;
                }
            }
        }
        // 如果不具备权限,就抛出AccessDeniedException异常
        throw new AccessDeniedException("权限不足");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}
④配置Spring Security

这里与前文的配置相比,主要是修改了configure(HttpSecurity http)方法的实现并添加了两个 Bean。至此实现了动态权限配置,权限和资源的关系可以在role_resource表中动态调整

// 配置基于数据库的URL访问权限
@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests()
		.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
		
		@Override
		public <O extends FilterSecurityInterceptor> O postProcess(O object) {
			object.setSecurityMetadataSource(accessMustRoles());
			object.setAccessDecisionManager(rolesCheck());
			return object;
		}
		
	})
	.and().formLogin().loginProcessingUrl("/login").permitAll()  // 开启表单登录并配置登录接口
	.and().csrf().disable();  // 关闭csrf
}

@Bean
public CustomFilterInvocationSecurityMetadataSource accessMustRoles() {
	return new CustomFilterInvocationSecurityMetadataSource();
}

@Bean
public CustomAccessDecisionManager rolesCheck() {
	return new CustomAccessDecisionManager();
}

要配置角色继承关系,只需在Spring Security的配置类中提供一个RoleHierarchy即可

// 配置角色继承关系
@Bean
RoleHierarchy roleHierarchy() {
	RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
	String hierarchy = "ROLE_DBA > ROLE_ADMIN > ROLE_USER";
	roleHierarchy.setHierarchy(hierarchy);
	return roleHierarchy;
}
⑤自定义登录页面、登录接口、登录成功或失败的处理逻辑

首先修改 Spring Security 配置,增加相关的自定义代码:
将登录页改成使用自定义页面,并配置登录请求处理接口,以及用户密码提交时使用的参数名
自定义了登录成功、登录失败的处理逻辑,根据情况返回响应的JSON数据

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 指定密码的加密方式
    @SuppressWarnings("deprecation")
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();  // 不对密码进行加密
    }

    // 配置用户及其对应的角色
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("root").password("123").roles("DBA")
                .and()
                .withUser("admin").password("123").roles("ADMIN")
                .and()
                .withUser("hangge").password("123").roles("USER");
    }

    // 配置URL访问权限
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() // 开启HttpSecurity配置
                .antMatchers("/db/**").hasRole("DBA") // db/** 模式URL需DBA角色
                .antMatchers("/admin/**").hasRole("ADMIN") // admin/**模式URL需ADMIN角色
                .antMatchers("/user/**").hasRole("USER") // user/** 模式URL需USER角色
                .anyRequest().authenticated() // 用户访问其它URL都必须认证后访问(登录后访问)
                .and().formLogin()  // 开启登录表单功能
                .loginPage("/login_page") // 使用自定义的登录页面,不再使用SpringSecurity提供的默认登录页
                .loginProcessingUrl("/login") // 配置登录请求处理接口,自定义登录页面、移动端登录都使用该接口
                .usernameParameter("name") // 修改认证所需的用户名的参数名(默认为username)
                .passwordParameter("passwd") // 修改认证所需的密码的参数名(默认为password)
                // 定义登录成功的处理逻辑(可以跳转到某一个页面,也可以返会一段JSON)
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest req,
                                                        HttpServletResponse resp,
                                                        Authentication auth)
                            throws IOException, ServletException {
                        // 可以跳转到指定页面
                        // resp.sendRedirect("/index");
                        // 也可以返回一段JSON提示
                        // 获取当前登录用户的信息,在登录成功后,将当前登录用户的信息一起返回给客户端
                        Object principal = auth.getPrincipal();
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        resp.setStatus(200);
                        Map<String, Object> map = new HashMap<>();
                        map.put("status", 200);
                        map.put("msg", principal);
                        ObjectMapper om = new ObjectMapper();
                        out.write(om.writeValueAsString(map));
                        out.flush();
                        out.close();
                    }
                })
                // 定义登录失败的处理逻辑(可以跳转到某一个页面,也可以返会一段 JSON)
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest req,
                                                        HttpServletResponse resp,
                                                        AuthenticationException e)
                            throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        resp.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 mapper = new ObjectMapper();
                        out.write(mapper.writeValueAsString(map));
                        out.flush();
                        out.close();
                    }
                })
				// 配置一个 LogoutHandler,开发者可以在这里完成一些数据清除工做
				.addLogoutHandler(new LogoutHandler() {
						@Override
						public void logout(HttpServletRequest req,HttpServletResponse resp,Authentication auth) {
							System.out.println("注销登录,开始清除Cookie。");
						}
				})
				// 配置一个 LogoutSuccessHandler,开发者可以在这里处理注销成功后的业务逻辑
				.logoutSuccessHandler(new LogoutSuccessHandler() {
					@Override
					public void onLogoutSuccess(HttpServletRequest req,HttpServletResponse resp,Authentication auth)
											throws IOException, ServletException {
						// 可以跳转到登录页面
						// resp.sendRedirect("/login");
						// 也可以返回一段JSON提示
						resp.setContentType("application/json;charset=utf-8");
						PrintWriter out = resp.getWriter();
						resp.setStatus(200);
						Map<String, Object> map = new HashMap<>();
						map.put("status", 200);
						map.put("msg", "注销成功!");
						ObjectMapper om = new ObjectMapper();
						out.write(om.writeValueAsString(map));
						out.flush();
						out.close();
					}
				})
                .permitAll() // 允许访问登录表单、登录接口
                .and().csrf().disable(); // 关闭csrf
    }
}

3.用户实体类(实现UserDetails)

UserDetails接口封装了Spring Security登录所需要的所有信息。如果在数据库中取用户数据,用户实体类需要实现该接口

public class User implements UserDetails {
	private Integer id;
	private String userName;
	private String password;
	private boolean enable;
	private boolean locked;
	private Set<Role> userRoles;
    // 省略...
}

省略数据库操作

package org.springframework.security.core.userdetails;

// 该接口封装了SpringSecurity登录所需要的所有信息。如果在数据库中取用户数据,用户实体类需要实现该接口
public interface UserDetails extends Serializable {
	// 获取用户权限信息
	Collection<? extends GrantedAuthority> getAuthorities();
	// 获取用户密码
	String getPassword();
	// 获取用户名称
	String getUsername();
	// 用户是否过期(true未过期,false过期)
	boolean isAccountNonExpired();
	// 用户是否锁定
	boolean isAccountNonLocked();
	// 密码是否过期
	boolean isCredentialsNonExpired();
	// 用户是否可用
	boolean isEnabled();
}

默认情况下不需要开发者自己进行密码角色等信息的比对,开发者只需要提供相关信息即可,例如:

方法描述
getPassword()返回密码和用户输入的登录密码不匹配,会自动抛出BadCredentialsException异常
isAccountNonLocked()返回了false,会自动抛出AccountExpiredException异常
getAuthorities()用来获取当前用户所具有的角色信息

本案例中,用户所具有的角色存储在roles属性中,因此该方法直接遍历roles属性,然后构造SimpleGrantedAuthority集合并返回


4.用户校验逻辑业务(实现UserDetailsService接口)

定义的UserService实现UserDetailsService接口,并实现该接口中的loadUserByUsername方法,该方法将在用户登录时自动调用。

loadUserByUsername()方法的参数就是用户登录时输入的用户名,通过用户名去数据库中查找用户:
①:如果没有查找到用户,就抛出一个账户不存在的异常。
②:如果查找到了用户,就继续查找该用户所具有的角色信息,并将获取到的user对象返回,再由系统提供的DaoAuthenticationProvider类去比对密码是否正确

在这里插入图片描述

@Service
@Slf4j
public class UserService implements UserDetailsService {
	@Autowired
	private PasswordEncoder passwordEncoder;
	@Autowired
	private UserMapper userMapper;

    /**
     * 根据用户名查找用户信息,该用户信息可以从数据库中取出,然后拼装成UserDetails对象
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// 先设置假的权限
        List<GrantedAuthority> authorities = new ArrayList<>();
        // 传入角色
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        // 创建用户
        User user = new User(username, "{noop}admin", authorities) ;

        log.info("登录用户名:" + username);
        // 下面User类是Spring Security自带实现UserDetails接口的一个用户类
        // 构造方法①:
		// 使用加密工具对密码进行加密
		String password = passwordEncoder.encode("123456");
		log.info("密码:" + password);
		        return new User(username,password
		                , AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
		// 构造方法②:
		return new User(username,passwordEncoder.encode("123456")
		, true, true, true, true
		, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
		// 实际业务③:
		User user = userMapperDao.loadUserByUsername(username);
		if (user == null) {
		throw new UsernameNotFoundException("账户不存在!");
		}
		// 数据库用户密码没加密,这里手动设置
		String encodePassword = passwordEncoder.encode(user.getPassword());
		System.out.println("加密后的密码:" + encodePassword);
		user.setPassword(encodePassword);
		Set<Role> userRoles = userMapperDao.getUserRolesByUid(user.getId());
		user.setUserRoles(userRoles);
		return user;
    }
}

经过接口访问后,然后换一个浏览器访问该接口;密码都是一样,加密出来却不一样。这主要要从BCryptPasswordEncoder加密和密码比对的两个方法来看

public class BCryptPasswordEncoder implements PasswordEncoder {
    private Pattern BCRYPT_PATTERN;
    private final Log logger;
    private final BCryptPasswordEncoder.BCryptVersion version;
    private final int strength; // 密码长度
    private final SecureRandom random; // 随机种子

public String encode(CharSequence rawPassword) {
      if (rawPassword == null) {
          throw new IllegalArgumentException("rawPassword cannot be null");
      } else {
          String salt;
          if (this.random != null) {
			  // 生成一个随机加盐的前缀,而使用SecureRandom来生成随机盐是较为安全的
              salt = BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
          } else {
              salt = BCrypt.gensalt(this.version.getVersion(), this.strength);
          }
		  // 根据随机盐与密码进行一次SHA256的运算并在之前拼装随机盐得到最终密码
		  // 因为每次加密,随机盐是不同的,不然不叫随机了,所以加密出来的密文也不相同
          return BCrypt.hashpw(rawPassword.toString(), salt);
      }
}

  public boolean matches(CharSequence rawPassword, String encodedPassword) {
      if (rawPassword == null) {
          throw new IllegalArgumentException("rawPassword cannot be null");
      } else if (encodedPassword != null && encodedPassword.length() != 0) {
          if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
              this.logger.warn("Encoded password does not look like BCrypt");
              return false;
          } else {
			  // 密码比对的时候,先从密文中拿取随机盐,而不是重新生成新的随机盐
			  // 再通过该随机盐与要比对的密码进行一次Sha256的运算,再在前面拼装上该随机盐与密文进行比较
              return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
          }
      } else {
          this.logger.warn("Empty encoded password");
          return false;
      }
  }
}

这里面的重点在于密文没有掌握在攻击者手里,是安全的,也就是攻击者无法得知随机盐是什么,而SecureRandom产生伪随机的条件非常苛刻,一般是一些计算机内部的事件。但是这是一种慢加密方式,对于要登录吞吐量较高的时候无法满足需求,但要说明的是MD5已经不安全了,可以被短时间内(小时记,也不是几秒内)暴力破解

Spring Security自带User说明

public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
    this(username, password, true, true, true, true, authorities);
}

1、用户名:提供给DaoAuthenticationProvider
2、密码:应该提供给用户的密码DaoAuthenticationProvider
3、是否可用/启用(true启用,false不启用) 
4、账户是否过期(true未过期,false过期)
5、密码是否过期(true未过期,false过期)
6、账户是否被锁定(true未锁定,false锁定)
7、用户权限:如果提供了正确的用户名和密码并启用了用户,则应授予用户权限。不为空
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
    if (username != null && !"".equals(username) && password != null) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.credentialsNonExpired = credentialsNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
    } else {
        throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
    }
}

5.编写接口类(Controller)

@Controller
public class MainController {
    @RequestMapping("/")
    public String root() {
        return "redirect:/index";
    }

    @ResponseBody
    @RequestMapping("/index")
    public String index() {
        return "当前为未经安全认证的页面";
    }

    @ResponseBody
    @RequestMapping("/user/index")
    public String userIndex() {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        return "user/index";
    }

    @RequestMapping("/login")
    public String login() {
        return "login";
    }

    @ResponseBody
    @RequestMapping("/login-error")
    public String loginError(Model model) {
		//  model.addAttribute("loginError", true);
		return "login-error";
    }
}

6.登录页面

位置:src/main/resources/templates/login.html
前后端分离:http://locahost/login
重新启动项目,访问/hello接口,被转向到指定的html登录页面。输入用户名user,密码123456后,/hello接口访问成功

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
    <head>
        <title>Login page</title>
        <meta charset="utf-8" />
        <link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
    </head>
    <body>
        <h1>Login page</h1>
        <form th:action="@{/login}" method="post">
            <label for="username">Username</label>:
            <input type="text" id="username" name="username" autofocus="autofocus" />
			<br />
            <label for="password">Password</label>:
            <input type="password" id="password" name="password" /> <br />
            <input type="submit" value="Log in" />
        </form>
        <p><a href="/index" th:href="@{/index}">Back to home page</a></p>
    </body>
</html>

7.效果

  • 当直接访问/user/index页面的时候会因为安全配置重定向到login页面
  • UserService只配置了admin/test的用户,只有用admin用户才能登录成功
  • 登录成功之后再访问/user/index页面才生效

在这里插入图片描述


8.测试授权功能

在userDetailsService中返回结果去掉ROLE_USER权限即可

调试关键点

方法描述
org.springframework.security.access.vote.AffirmativeBased#decide判断用户是否有权访问当前接口
org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks密码加密校验
/**
 * anyRequest          |   匹配所有请求路径
 * access              |   SpringEl表达式结果为true时可以访问
 * anonymous           |   匿名可以访问
 * denyAll             |   用户不能访问
 * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
 * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
 * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
 * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
 * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
 * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
 * permitAll           |   用户可以任意访问
 * rememberMe          |   允许通过remember-me登录的用户访问
 * authenticated       |   用户登录后可访问
 */
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    // 开启HttpSecurity配置
    httpSecurity.authorizeRequests()
            // 不进行权限验证的请求或资源
            .antMatchers(AUTH_WHITE_LIST).permitAll()
            // 指定匿名用户允许URL
            .antMatchers(AUTH_ANONYMOUS).anonymous()
            // 其他的需要登陆后才能访问
            .anyRequest().authenticated()
            .and()
            // 配置未登录自定义处理类
            .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
            .and()
            // 配置登录地址
            .formLogin()
            .loginPage("/login")
            // 配置登录成功自定义处理类
            .successHandler(userLoginSuccessHandler)
            // 配置登录失败自定义处理类
            .failureHandler(userLoginFailureHandler)
            .and()
            // 配置登出地址
            .logout()
            .logoutUrl("/logout")
            // 设置注销成功后跳转页面,默认是跳转到登录页面
            .logoutSuccessUrl("/login")
            // 配置用户登出自定义处理类
            .logoutSuccessHandler(userLogoutSuccessHandler)
            .and()
            // 配置没有权限自定义处理类
            .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)
            .and()
            // 开启跨域
            .cors()
            .and()
            // 关闭跨站请求伪造防护
            .csrf().disable();
    // 配置登出地址
    httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(userLogoutSuccessHandler);
    // 基于Token不需要session
    httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    // 禁用缓存
    httpSecurity.headers().cacheControl();
    // 添加JWT过滤器
    httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    // 添加CORS filter
    httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
    httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}

登录成功处理类(AuthenticationSuccessHandler)

@Component
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {
    /**
     * 登录成功返回结果
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        // 组装JWT
        SelfUserEntity selfUserEntity =  (SelfUserEntity) authentication.getPrincipal();
        String token = JWTTokenUtil.createAccessToken(selfUserEntity);
        token = JWTConfig.tokenPrefix + token;

        // 封装返回参数
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","200");
        resultData.put("msg", "登录成功");
        resultData.put("token",token);
        ResultUtil.responseJson(response,resultData);
    }
}

登录失败处理类(AuthenticationFailureHandler)

@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
    /**
     * 登录失败返回结果
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){
        // 这些对于操作的处理类可以根据不同异常进行不同处理
        if (exception instanceof UsernameNotFoundException){
            System.out.println("【登录失败】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户名不存在"));
        }
        if (exception instanceof LockedException){
            System.out.println("【登录失败】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户被冻结"));
        }
        if (exception instanceof BadCredentialsException){
            System.out.println("【登录失败】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"密码错误"));
        }
        ResultUtil.responseJson(response,ResultUtil.resultCode(500,"登录失败"));
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/456471.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Unity入门】15.鼠标输入和键盘输入

【Unity入门】鼠标输入和键盘输入 大家好&#xff0c;我是Lampard~~ 欢迎来到Unity入门系列博客&#xff0c;所学知识来自B站阿发老师~感谢 &#xff08;一&#xff09;监听鼠标输入 (1) Input类 Unity的Input类提供了许多监听用户输入的方法&#xff0c;比如我们常见的鼠标&a…

redis复制的设计与实现

一、复制 1.1旧版功能的实现 旧版Redis的复制功能分为 同步&#xff08;sync&#xff09;和 命令传播。 同步用于将从服务器更新至主服务器的当前状态。命令传播用于 主服务器状态变化时&#xff0c;让主从服务器状态回归一致。 1.1.1同步 当客户端向服务端发送slaveof命令…

5款十分小众的软件,知道的人不多但却很好用

今天推荐5款十分小众的软件&#xff0c;知道的人不多&#xff0c;但是每个都是非常非常好用的&#xff0c;有兴趣的小伙伴可以自行搜索下载。 1.视频直播录制——OBS Studio OBS Studio可以让你轻松地录制和直播你的屏幕、摄像头、游戏等内容。你可以使用OBS Studio来创建多种…

opengl入门之创建窗口

OpenGL系列文章目录 文章目录 OpenGL系列文章目录前言GLFWGLFW Logo构建GLFWAttention编译生成的库CMake编译我们的第一个工程链接Windows上的OpenGL库Linux上的OpenGL库ImportantGLAD配置GLAD 附加资源 前言 注意&#xff0c;由于作者对教程做出了更新&#xff0c;之前本节使…

springboot使用jasper实现报表demo

概述 业务中尝尝需要用到报表数据的渲染和导出.报表的配置势必不能写死&#xff0c;需要动态配置。 现成的JasperReports Jaspersoft Studio即可实现可配置的报表。 报表布局Jaspersoft Studio https://community.jaspersoft.com/community-download 下载布局工具&#xf…

【fisco-bcos底层链】WeBASE管理平台各组件分别打包成镜像部署到k8s上亲测完成

【fisco-bcos底层链】WeBASE管理平台各组件分别打包成镜像部署到k8s上亲测完成 前言第一步、docker 打包dockerfile书写第二步、fisco-bcos 三节点的底层链1. 更改数据库连接信息2.删除节点中运行预语句中最后的的&符号3. 具体编译的dockerfile4. 编译dockerfile生成 fisco…

【Linux编程Shell自动化脚本】01 Shell 变量、条件语句及常用概念操作等详解

文章目录 一、简介二、变量详解1. 系统变量 三、If条件语句1. ()、(())、[]、[[]]、let和test的区别&#xff08;1&#xff09;bash中的Compound Commands&#xff08;2&#xff09;Shell Builtin Commands&#xff08;3&#xff09;Arithmetic Evaluation&#xff08;4&#x…

【MySQL | 进阶篇】09、MySQL 管理及常用工具(mysqladmin、mysqlbinlog、mysqldump 等)的使用

目录 一、系统数据库 二、常用工具 2.1 mysql 示例 2.2 mysqladmin 示例 2.3 mysqlbinlog 示例 2.4 mysqlshow 示例 2.5 mysqldump&#xff08;数据备份&#xff09; 示例 2.6 mysqlimport/source&#xff08;数据恢复&#xff09; 2.6.1 mysqlimport 2.6.2 …

实现表格可编辑(点击字段出现输入框)vue elementUI

实现表格可编辑 参考&#xff1a;el-table 中实现表格可编辑_el表格编辑_快乐征途的博客-CSDN博客 按行保存数据&#xff0c;每行数据最后都有一个保存按钮&#xff0c;如下图所示 使用的是<template>嵌套<el-input> 完整代码如下&#xff1a; <template>…

【计算机视觉 | 目标检测】RT-DETR:DETRs Beat YOLOs on Real-time Object Detection

文章目录 一、前言二、简介三、相关方法3.1 实时目标检测器3.2 端到端目标检测器3.3 目标检测的多尺度特征 四、检测器端到端速度4.1 分析NMS4.2 端到端速度基准 五、The Real-time DETR5.1 方法概览5.2 高效混合编码器5.2.1 计算瓶颈分析5.2.2 Hybrid design5.2.3 IoU-Aware查…

【LeetCode】105. 从前序与中序遍历序列构造二叉树

1.问题 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 示例 1 输入: preorder [3,9,20,15,7], inorder [9,3,15,20,7] 输出: [3,9,20,null,null…

“访达”不能完成操作,因为不能读取或写入“”中某些数据。

“访达”不能完成操作&#xff0c;因为不能读取或写入“”中某些数据。 复制文件夹时出现「“访达”不能完成该操作&#xff0c;因为不能读取或写入“”中的某些数据。 &#xff08;错误代码-36&#xff09;」及icloud中文件夹无法下载提示「未能下载项目“XX”。」 最近1个月…

1.Mybatis基本介绍

为什么使用MyBatis&#xff1f; 1.JDBC在创建Connection的时候&#xff0c;存在硬编码问题&#xff08;也就是直接把连接信息写死&#xff0c;不方便后期维护&#xff09; 2.preparedStatement对象在执行sql语句的时候存在硬编码问题 3.每次在进行一次数据库连接后都会关闭数据…

【SWAT水文模型】ArcSWAT输入准备:土地利用/土壤/气象数据

ArcSWAT输入准备&#xff1a;土地利用/土壤/气象数据 1 土地利用数据的处理1.1 数据下载 2 土壤库建立2.1 数据下载 3 气象数据库参考 1 土地利用数据的处理 1.1 数据下载 下载地址如下&#xff1a; 中科院1km土地利用数据 清华大学高精度土地利用数据 2 土壤库建立 SW…

pytest学习二(通过配置文件运行、分组执行,及其它一些参数)

接上一篇说到了环境的配置&#xff0c;以及一个用例的编写&#xff0c;接下来继续记录一些它的运行方式和一些平常使用的标签 一、通过全局配置文件&#xff08;pytest.ini&#xff09;运行 pytest.ini配置文件的编写规则 ①编码格式一般为ANSI ②一般放在项目的根目录下&a…

个人杂笔记

docker里面的-p暴露端口是确确实实写了才会映射到主机 docker run -d --hostname my-rabbit --name my-rabbit -e RABBITMQ_DEFAULT_USERroot -e RABBITMQ_DEFAULT_PASS250772730 -p 8080:8080 -p 15672:15672 -p 5672:5672 rabbitmq:3-managementpip安装提示warning 可能原因…

【 Spring Mybatis 复杂的查询操作 】

文章目录 引言一、参数占位符 #{} 和 ${}二、SQL 注入三、like 模糊查询四、返回类型&#xff1a;resultType 和 resultMap五、多表查询 引言 前面我们已经学会了使用 Mybatis 进行增&#xff0c;删&#xff0c;改操作&#xff0c;也实现了简单的查询操作 &#xff01;下面我们…

执着于考研考公却一再挫败,拿什么拯救你的职场后半生?

今天之所以想写一篇这样的文章&#xff0c;确确实实是有感而发&#xff0c;因为从近来接触的学员身上&#xff0c;能够最直观地感受到&#xff1a;考公考研失败后的同学&#xff0c;他们内心的那种焦虑感远超往期&#xff01; 用他们的话讲&#xff1a;“目前的状态就是感觉自…

540. 有序数组中的单一元素

给你一个仅由整数组成的有序数组&#xff0c;其中每个元素都会出现两次&#xff0c;唯有一个数只会出现一次。 请你找出并返回只出现一次的那个数。 你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。 示例 1: 输入: nums [1,1,2,3,3,4,4,8,8] 输出: 2 示…

“开发人员必备技能:Python接口自动化测试全攻略“:了解接口测试的基础知识,并通过Python编写测试用例,提升自己的测试技能

目录 摘要 一、基础知识 二、工具选择 三、实现步骤 1.安装依赖库 2.编写测试用例 3.运行测试用例 4.查看测试结果 四、代码实现 总结 摘要 随着互联网行业的不断发展&#xff0c;越来越多的企业开始注重自动化测试的重要性。而在自动化测试中&#xff0c;接口自动化…