想着既然项目中用到Spring Security框架,那为了以后面试说到这个项目的时候不害怕被问框架问题,干脆就好好学一下,基础、实战、源码分析一个一个来。
Spring Security参考手册:
https://www.springcloud.cc/spring-security-zhcn.html#true-java
https://docs.spring.io/spring-security/site/docs/5.1.12.RELEASE/reference/htmlsingle/#jc-authentication-userdetailsservice
Spring Security社区
http://www.spring4all.com/article/428
《Spring Security实战》
Spring Security前身是Acegi Security,在被收纳为Spring子项目后正式更名为Spring Security
Spring Security,一种基于Spring AOP和Servlet过滤器的安全框架,可提供全面的安全性解决方案,在Web请求级和方法调用级处理身份认证和授权
Spring Security是一个框架,致力于为Java应用程序提供认证和授权,与所有Spring项目一样,SS真正强大之处在于可轻松扩展以满足自定义需求
其安全性体现在两方面:认证和授权,这两个概念并非Spring Security(后续简称SS)独有,只是在SS中可更便捷的完成认证和授权
认证:确认某主体在某系统中是否合法、可用,此处主体可为登录系统的用户,也可为接入的设备或其他系统 即验证登录 SS集成支持多种认证方式
HTTP BASIC authentication headers:基于IETF RFC的标准
IETF:Internet Engineering Task Force,Internet工程任务组,推动Internet标准规范制定的主要组织
RFC:Request For Comments,请求注解,包含大多数关于Internet的重要文字资料,“网络知识圣经”
LDAP:跨平台身份验证方式
Form-based authentication:简单的用户界面需求
OpenID authentication:去中心化的身份认证方式
Jasig Central Authentication Service:单点登录方案
Kerberos:使用对称密钥机制,允许客户端与服务器相互确认身份的认证协议
授权:主体通过认证后,是否允许其执行某项操作的过程 即赋予权限,SS支持基于URL对Web的请求授权,方法访问授权,对象访问授权等
SS的特征:
全面和可扩展的身份认证和授权支持
防止攻击,例如会话固定、CSRF、点击劫持等
集成Servlet API
可选与Spring Web MVC集成
1. 使用SpringBoot 的Initializr搭建项目 https://start.spring.io/
1.1 Security作为构建项目的最小依赖
1.2 Web作为构建项目的核心
pom.xml依赖如下:
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
项目的结构图如下:
项目入口如下:
@RestController
@SpringBootApplication
public class SpringsecurityApplication {
@GetMapping("/")
public String testHello(){
return "Hello, spring security";
}
public static void main(String[] args) {
SpringApplication.run(SpringsecurityApplication.class, args);
}
}
2. 如果不配置用户名和密码,系统默认登录用户名为user,密码为动态生成并打印到控制台的一串随机码
3. 如果配置用户名和密码,使用配置的用户名和密码进行认证
// application.properties
spring.security.user.name=wx
spring.security.user.password=ciery123
4. 实际中绝大多数Web应用不会使用HTTP基本认证这种方式,灵活性不足、安全性差、无法携带cookie。基本更倾向于选择表单认证,自己实现表单的登录页和验证逻辑从而提高安全性
1. SS支持HTTP基本认证和Form表单认证,如果不进行自定义配置只是单纯继承默认配置的话,SS默认使用Form表单认证
@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
2. 重新运行程序,输入hello请求,自动跳转SS默认的login界面,输入用户名和密码,系统进行验证
1. 自定义http配置,配置loginPage,手动编写登录页面并放行登录页面,除开登录页面都需要进行认证(登录页面本身就是进行认证工作,如果还不给放行就离谱了)
@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
// 自定义登录配置
// authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
http.authorizeRequests()
// 任意一个http请求都会进行认证
.anyRequest().authenticated()
.and()
// 设置表单验证登录
.formLogin()
// 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
.loginPage("/login.html")
// 设置登录页不设置访问限制,即登录页不用进行拦截
.permitAll()
.and()
// 禁止csrf攻击
.csrf().disable();
}
}
2. 编写login.html静态页面,放在resources/static目录下
login
3. 启动项目后,访问/hello资源跳转编写的login.html静态页面,提交用户名和密码,框架进行身份认证。如果认证成功则跳回/hello请求页面继续完成HTTP请求
4. 可指定处理登录请求的路径
4.1 如果需要自定义login的url,可以使用loginProcessingUrl进行配置
@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
// 自定义登录配置
// authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
http.authorizeRequests()
// 任意一个http请求都会进行认证
.anyRequest().authenticated()
.and()
// 设置表单验证登录
.formLogin()
// 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
.loginPage("/login.html")
// 指定处理登录请求的路径
.loginProcessingUrl("/login")
// 设置登录页不设置访问限制,即登录页不用进行拦截
.permitAll()
.and()
// 禁止csrf攻击
.csrf().disable();
}
}
4.2 login.html中需要修改action,url由/login.html改为自定义的"login"或"/login"
4.3 用户输入用户名和密码后,跳转自定义的认证路径完成认证 可以看到第3点中是login.html,此处已经被定义为login
5. 可自定义处理认证成功/失败
目前认证成功后,是由后端直接跳转返回之前访问的请求页面,但如果前后端完全分离的话仅靠JSON交互的系统中,这个是行不通的。这就需要后端在认证成功/失败后传给前端一个JSON,告诉前端目前登录认证的返回结果,前端根据这个结果进行相应的处理
可自定义实现SS自带的AuthenticationSuccessHandler和AuthenticationFailureHandler,利用HttpServletResponse对象将结果json反馈到页面上(后续前端是可以直接捕获这个json么?还是说需要后端调整将json传给前端?
@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
// 自定义登录配置
// authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
http.authorizeRequests()
// 任意一个http请求都会进行认证
.anyRequest().authenticated()
.and()
// 设置表单验证登录
.formLogin()
// 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
.loginPage("/login.html")
// 指定处理登录请求的路径url
.loginProcessingUrl("/login")
// 指定登录成功时的处理逻辑 登录成功后后端传递json给前端,然后前端再处理,而非默认后端让其直接调回原页面
.successHandler(new getAuthenticationSuccessHandler())
.failureHandler(new getAuthenticationFailureHandler())
// 设置登录页不设置访问限制,即登录页不用进行拦截
.permitAll()
.and()
// 禁止csrf攻击
.csrf().disable();
}
/**
* 创建实现AuthenticationSuccessHandler认证成功处理器类
*/
private class getAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
// 构造json "key" : "value"
httpServletResponse.getWriter().write("{\"error_code\":\"0\",\"message\":\"welcome\"}");
httpServletResponse.sendRedirect("/hello");
}
}
/**
* 创建实现AuthenticationFailureHandler认证失败处理器类 由于两个处理器类只有一个方法,可以使用匿名内部类直接在configure方法中重写成功/失败的处理
*/
private class getAuthenticationFailureHandler implements AuthenticationFailureHandler{
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
// 401表示未登录
httpServletResponse.setStatus(401);
httpServletResponse.getWriter().write("{\"error_code\":\"401\",\"name\": \"" + e.getClass() + "\",\"message\":\"" + e.getMessage() + "\"}");
}
}
}
当然由于两个Handler只有一个方法,可以使用匿名内部类来实现对接口的重写
@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
// 自定义登录配置
// authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
http.authorizeRequests()
// 任意一个http请求都会进行认证
.anyRequest().authenticated()
.and()
// 设置表单验证登录
.formLogin()
// 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
.loginPage("/login.html")
// 指定处理登录请求的路径
.loginProcessingUrl("/login")
// 指定登录成功时的处理逻辑 登录成功后后端传递json给前端,然后前端再处理,而非默认后端让其直接调回原页面
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
// 构造json "key" : "value"
httpServletResponse.getWriter().write("{\"error_code\":\"0\",\"message\":\"welcome\"}");
httpServletResponse.sendRedirect("/hello");
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
// 401表示未登录
httpServletResponse.setStatus(401);
httpServletResponse.getWriter().write("{\"error_code\":\"401\",\"name\": \"" + e.getClass() + "\",\"message\":\"" + e.getMessage() + "\"}");
}
})
// 设置登录页不设置访问限制,即登录页不用进行拦截
.permitAll()
.and()
// 禁止csrf攻击
.csrf().disable();
}
5.1 如果认证成功,返回信息到前端并跳转hello页面
5.2 如果认证失败,返回错误提示信息到前端
5.3 在successHandler中参数包含Authentication参数,携带当前登录的用户名和角色信息,可以根据需求进行处理,例如此处如果登录成功打印用户名
/**
* 创建实现AuthenticationSuccessHandler认证成功处理器类
*/
private class getAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
// 构造json "key" : "value" 注意 + 拼接的时候左右要加 \"
httpServletResponse.getWriter().write("{\"error_code\":\"0\",\"message\":\"welcome, \"" + ((UserDetails)authentication.getPrincipal()).getUsername() + "\"}");
// 最好后端不进行页面跳转,否则会覆盖json,直接返回json给前端,前端根据json信息进行处理即可
// httpServletResponse.sendRedirect("/hello");
}
}
1. 配置三个访问controller接口,其中user接口只可具有user权限的用户访问,admin接口只可具有admin权限的用户访问
@RestController
@RequestMapping("/app")
public class AppController {
@GetMapping("/testApp")
public String testApp(){
return "hello,app";
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/testUser")
public String testUser(){
return "hello,user";
}
}
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/testAdmin")
public String testAdmin(){
return "hello,admin";
}
}
2. 根据访问权限限制配置configure,使用antMatchers,填写需要访问的controller的url,添加访问角色验证
protected void configure(HttpSecurity http) throws Exception {
// 自定义登录配置
// authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
http.authorizeRequests()
/* antMatchers是一个采用ANT模式的URL匹配器
?匹配任意单个字符
*匹配0或任意数量的字符
**匹配0或更多的目录
.antMatchers("/app/**") 相当于匹配/app/下所有的API
permitAll()表示公开权限
hasRole("role_name")限定只有方法参数role_name角色的用户才可访问
*/
.antMatchers("/app/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
// 任意一个http请求都会进行认证
.anyRequest().authenticated()
.and()
// 设置表单验证登录
.formLogin()
// 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
.loginPage("/login.html")
// 指定处理登录请求的路径
.loginProcessingUrl("/login")
// 设置登录页不设置访问限制,即登录页不用进行拦截
.permitAll()
.and()
// 禁止csrf攻击
.csrf().disable();
}
3. 注意注释掉前面在application.properties中添加的默认username和password 开始创建多用户支持,分为基于内存、基于SS自带的JDBC数据库模型、基于自定义的数据库模型
由于基于内存所以每次重启项目内存被清空,用户都会重新创建。重启项目,user用户可以进入user/testUser,访问admin/testAdmin显示403错误
2xx:访问成功
4xx:浏览器错误 401未登录 403无权限 404无页面
5xx:服务器错误
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用明文,不对密码进行加密处理
auth.inMemoryAuthentication().passwordEncoder(NoOpPasswordEncoder.getInstance())
.withUser("user").password("123456").roles("USER")
.and()
.withUser("admin").password("123456").roles("ADMIN");
}
由于数据库的主键限制,创建过的用户不可再通过configure自动创建(主键冲突)即重启项目就会报错。所以需要进行用户是否存在验证,如果存在则不作任何处理;不存在则创建
2.1 添加数据库依赖
//pom.xml
org.springframework.boot
spring-boot-starter-jdbc
mysql
mysql-connector-java
2.2 根据框架定义的数据表要求创建users和authorities表,唯一索引的作用是加快查询
2.3 添加数据库连接信息
// application.properties 配置数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/springsecuritydemo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
spring.datasource.username=root
spring.datasource.password=root
2.4 创建用户信息
// WebSecurityConfig
@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用明文,不对密码进行加密处理
// getUserDetailsService()方法从spring容器中获取jdbcManager
auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(NoOpPasswordEncoder.getInstance()).getUserDetailsService();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义登录配置
// authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
http.authorizeRequests()
.antMatchers("/app/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
// 任意一个http请求都会进行认证
.anyRequest().authenticated()
.and()
// 设置表单验证登录
.formLogin()
// 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
.loginPage("/login.html")
// 指定处理登录请求的路径
.loginProcessingUrl("/login")
// 设置登录页不设置访问限制,即登录页不用进行拦截
.permitAll()
.and()
// 禁止csrf攻击
.csrf().disable();
}
@Bean
public UserDetailsService userDetailsService(){
JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
manager.setDataSource(dataSource);
// 创建用户
// 如果user存在则不作任何操作,如果不存在则创建用户,底层调用insert into users(usernmae,password,enable) values(?,?,?); 如果有role则调用insert into authorities (username, authority) values (?,?)
if(!manager.userExists("user")){
// User对象通过withUsername,password等方法给其赋值
manager.createUser(User.withUsername("user").password("123456").roles("USER").build());
}
if(!manager.userExists("admin")){
manager.createUser(User.withUsername("admin").password("123456").roles("ADMIN").build());
}
return manager;
}
}
2.5 自动插入到users和authorites表中的数据
拓展:数据能插入到数据库的原因:底层调用了JdbcTemplate类的相关insert/update方法将配置中的数据插入到数据表中
// JdbcUserDetailsManager
private String createUserSql = "insert into users (username, password, enabled) values (?,?,?)";
private String createAuthoritySql = "insert into authorities (username, authority) values (?,?)";
public void createUser(UserDetails user) {
this.validateUserDetails(user);
this.getJdbcTemplate().update(this.createUserSql, (ps) -> {
ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword());
ps.setBoolean(3, user.isEnabled());
int paramCount = ps.getParameterMetaData().getParameterCount();
if (paramCount > 3) {
ps.setBoolean(4, !user.isAccountNonLocked());
ps.setBoolean(5, !user.isAccountNonExpired());
ps.setBoolean(6, !user.isCredentialsNonExpired());
}
});
if (this.getEnableAuthorities()) {
this.insertUserAuthorities(user);
}
}
private void insertUserAuthorities(UserDetails user) {
Iterator var2 = user.getAuthorities().iterator();
while(var2.hasNext()) {
GrantedAuthority auth = (GrantedAuthority)var2.next();
this.getJdbcTemplate().update(this.createAuthoritySql, new Object[]{user.getUsername(), auth.getAuthority()});
}
}
自定义UserDetails实现类User,然后自行通过UserServiceImpl类的loadByUsername方法进行登录认证
3.1 设计数据库user
3.2 添加mybatis依赖和自动映射mybatis-generator插件
// pom.xml
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.3
org.mybatis.generator
mybatis-generator-maven-plugin
1.3.2
true
true
src/main/resources/mybatis-generator.xml
mysql
mysql-connector-java
8.0.17
3.3 创建映射配置文件并生成实体类User、Dao层UserMapper类、数据库映射文件UserMapper.xml
3.3.1 创建jdbc.properties添加数据库连接信息,之前的application.properties中的可以删去
// jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/springsecuritydemo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT
jdbc.username=root
jdbc.password=root
3.3.2 mybatis-generator.xml
// mybatis-generator.xml
3.3.3 application.properties中添加mapper类的映射对应位置
// application.properties
#配置mybatis(相当于普通MyBatis中的SqlMapConfig.xml主配置文件中指定映射配置文件的地址和别称) 不配置会绑定数据失败,运行时无法找到mapper类下的方法(编译不报错,将实体bean加入spring容器后 解耦的效果)
mybatis.mapper-locations=classpath:/mapper/*.xml
mybatis.type-aliases-package= com.practice.mall.domain
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.use-generated-keys=true
mybatis.configuration.cache-enabled=false
3.3.4 开启mapper类的扫描
@MapperScan("com.practice.springsecurity.mapper")
@SpringBootApplication
public class SpringsecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityApplication.class, args);
}
}
3.4 实体类User实现UserDetails并添加List类型的GrantAuthority权限集属性
public class User implements UserDetails {
private Long id;
private String username;
private String password;
private Boolean enabled;
private String roles;
//权限
private List authorities;
public void setAuthorities(List authorities) {
this.authorities = authorities;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getRoles() {
return roles;
}
public void setRoles(String roles) {
this.roles = roles == null ? null : roles.trim();
}
}
3.5 创建UserDetailsServiceImpl实现框架自带的UserDetailsService接口,重写loadByUsername方法
@Service //加入spring容器
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 根据username进行用户认证
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中根据username查找用户
User user = userMapper.selectByUsername(username);
// 验证user是否存在
if(user != null){
// 将数据库中的role解析为权限集
// AuthorityUtils.commaSeparatedStringToAuthorityList可将用逗号隔开的权限集字符串切割成可用的权限对象列表
// 也可自定义实现切分方式,同步user表中的roles字段
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
// 用户不存在
throw new UsernameNotFoundException("用户不存在");
}
}
3.6 在WebSecurityConfig配置类中将自定义的认证方式注入框架的authbuilder对象
@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Autowired
UserDetailsServiceImpl userDetailsServiceImpl;
/**
* 建立身份认证方式 AuthenticationManagerBuilder创建一个AuthenticationManager用于进行身份认证
* 包括:内存验证、LDAP验证、基于JDBC的验证、添加UserDetailsService、添加AuthenticationProvider
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用明文,不对密码进行加密处理
auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(NoOpPasswordEncoder.getInstance());
}
/**
* 开启认证 配置需要认证的url
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
// 自定义登录配置
// authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
http.authorizeRequests()
.antMatchers("/app/**").permitAll()
// 限制既有ADMIN又有USER的用户访问
.antMatchers("/admin/**").access("hasRole('ADMIN') and hasRole('USER')")
.antMatchers("/user/**").hasRole("USER")
// 任意一个http请求都会进行认证
.anyRequest().authenticated()
.and()
// 设置表单验证登录
.formLogin()
// 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
.loginPage("/login.html")
// 指定处理登录请求的路径
.loginProcessingUrl("/login")
// 设置登录页不设置访问限制,即登录页不用进行拦截
.permitAll()
.and()
// 禁止csrf攻击
.csrf().disable();
}
3.7 数据库user表中添加测试数据
3.8 测试
3.8.1 user登录admin/testAdmin,根据username从数据库获取user对象,并获取其roles匹配这个url需要的角色匹配不成功,则用户user登录成功但是无权限显示403
3.8.2 user登录user/testUesr,根据username从数据库获取user对象,并获取其roles匹配这个url需要的角色信息匹配成功,则用户user登录成功并可访问这个url
数据表user的roles字段添加ROLE_前缀,但是配置类configure中hasRole("USER")方法没有使用前缀的原因:
查看hasRole处理逻辑,本身自带了ROLE_,将hasRole方法的参数拼接ROLE_然后和loadByUsername获取的user对象的authority进行匹配,如果匹配成功则该用户有权限放行;否则无权限显示403错误 (即存在可以登录但是无权限的情况)
// ExpressionUrlAuthorizationConfigurer
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
} else {
return "hasRole('ROLE_" + role + "')";
}
}
模拟使用场景,模拟面试环节多问几个为什么,培养看源码的良好习惯,学习大佬们的编程风格