1. 自定义登录页面
SpringSecurity默认会自动生成一个登录页面(/login),并使用默认URL处理登录的提交内容,登录后跳转到默认URL等。尽管SpringSecurity提供默认登录页面但在真实开发环境中不会使用默认页面,需要自己指定页面
1.1 认证页面准备
认证页面使用Thmeleaf模板
① thymeleaf的maven依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
② 结构:
③ login.jsp:
<!DOCTYPE html> <html lang="en" class="no-js"> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <title>Title</title> </head> <body> <form action="/security-springboot/login" method="post"> 用户名:<input type="text" name="username"> 密 码: <input type="password" name="password"> <input type="submit" value="登录"> </form> </body> </html>
1.2 配置认证页面
① 在实现了WebMvcConfigurer接口的配置文件中配置认证页面地址:
@Configuration public class WebConfig implements WebMvcConfigurer { //默认URL根据跳转到/login,此url为spring security提供 @Override public void addViewControllers(ViewControllerRegistry registry) { //输入项目地址自动重定向到登录页面 registry.addViewController("/").setViewName("redirect:/loginview"); //URL绑定登录页面 registry.addViewController("/loginview").setViewName("/login"); } }
② 在WebSecurityConfig中配置表单登录信息
@Configuration public class WebSecurityContext extends WebSecurityConfigurerAdapter { //3,安全拦截机制 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .formLogin()//允许表单登录 .loginPage("/loginview")//指定自定义登录页,security以重定向方式跳转到/login-view .loginProcessingUrl("/login")//指定登录处理URL,也就是用户名、密码表单提交的目的路径 .successForwardUrl("/login-success")//自定义登录成功的跳转页面地址 .permitAll();//允许任何用户访问表单登录页 } }
- formLogin():允许表单登录
- loginPage():指定自定义登录页,security以重定向方式跳转到指定页面
- loginProcessingUrl():指定登录处理URL,也就是用户名、密码表单提交的目的路径
- successForwardUrl():自定义登录成功的跳转页面地址
- permitAll():允许任何用户访问表单登录页
测试:
1.3 解决错误
问题出现的原因:SpringSecurity为防止CSRF(Cross-site request forgerf跨站请求伪造)的发生,限制了除get以外的大多数方法
1)解决方案1:屏蔽CSRF控制,即SpringSecurity不再限制CSRF
配置WebSecurityConfig
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable();//屏蔽CSRF控制 }
2)解决方案2:再页面请求时添加一个Token,SpringSecurity会验证token,如果token合法则可以继续请求
<form action="/security-springboot/login" method="post"> <input type="hidden" name="${_csrf.parameterName} value="${_csrf.token}"> </form>
2. 连接数据库认证
在案例中都是将用户信息使用硬编码的方式存储在内存中,实际项目中用户信息存储在数据库中,这次从数据库读取用户信息进行认证
2.1 创建数据表
表的SQL:
CREATE TABLE `security_user`( `id` bigint(20) NOT NULL COMMENT '用户id', `username` VARCHAR(64) NOT NULL, `password` VARCHAR(64) NOT NULL, `fullname` VARCHAR(255) NOT NULL, `mobile` VARCHAR(11) DEFAULT NULL COMMENT "手机号", PRIMARY KEY (`id`) USING BTREE )
2.2 数据库模块
① 导入依赖
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
② application.properties配置
spring.datasource.driver-class-name= com.mysql.jdbc.Driver spring.datasource.url= jdbc:mysql://localhost:3306/springcloud_test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 spring.datasource.username= root spring.datasource.password= 123
③ 定义pojo
@Data public class SecurityUserDao { private int id; private String username; private String password; private String fullname; private String mobile; }
④ 定义Dao
@Repository public class userDao { @Autowired JdbcTemplate jdbcTemplate; //根据用户名查询用户信息 public SecurityUserDao getUserByUserName(String username){ String SQL = "SELECT id,username,password,fullname,mobile FROM security_user WHERE username=?"; List<SecurityUserDao> query = jdbcTemplate.query(SQL, new Object[]{username}, new BeanPropertyRowMapper<SecurityUserDao>(SecurityUserDao.class)); if(query!=null && query.size()==1){ return query.get(0); } return null; }}
2.3 权限模块
① SpringSecurity的配置
@Configuration public class WebSecurityContext extends WebSecurityConfigurerAdapter { //2,密码编码器 @Bean public PasswordEncoder passwordEncoder(){ //不需要对密码进行编码 return NoOpPasswordEncoder.getInstance(); } //3,安全拦截机制 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/wql/**").authenticated()//所有/wql/**的请求必须认证通过 .anyRequest().permitAll()//除了/wql/))其他请求可以访问 .and() .formLogin()//允许表单登录 .loginPage("/loginview") .loginProcessingUrl("/login") .successForwardUrl("/login-success")//自定义登录成功的页面地址 .failureForwardUrl("/login-failure") .permitAll(); http.csrf().disable(); } }
@Service public class SpringDataUserDetailsService implements UserDetailsService { @Autowired userDao userDao; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { //1.将来连接数据库根据账号查询用户信息 SecurityUserDao userByUserName = userDao.getUserByUserName(s); if(userByUserName==null){ return null; } //2.登录账号 UserDetails build = User.withUsername(userByUserName.getUsername()).password(userByUserName.getPassword()).authorities("a1").build(); return build; } }
3. 连接数据库授权
3.1 数据库准备
① 用户表
CREATE TABLE `security_user`( `id` bigint(20) NOT NULL COMMENT '用户id', `username` VARCHAR(64) NOT NULL, `password` VARCHAR(64) NOT NULL, `fullname` VARCHAR(255) NOT NULL, `mobile` VARCHAR(11) DEFAULT NULL COMMENT "手机号", PRIMARY KEY (`id`) USING BTREE )
② 角色表
CREATE TABLE `t_role`( `id` VARCHAR(32) NOT NULL, `role_name` VARCHAR(255) DEFAULT NULL, `description` VARCHAR(255) DEFAULT NULL, `create_time` DATETIME DEFAULT NULL, `update_time` DATETIME DEFAULT NULL, `status` char(1) NOT NULL, PRIMARY KEY(`id`) )ENGINE=INNODB DEFAULT CHARSET=utf8
③ 用户角色关系表
CREATE TABLE `t_user_role`( `user_id` VARCHAR(32) NOT NULL, `role_id` VARCHAR(32) NOT NULL, `create_time` datetime DEFAULT NULL, `create` VARCHAR(255) DEFAULT NULL, PRIMARY KEY(`user_id`,`role_id`) )ENGINE=InnoDB DEFAULT CHARSET=utf8;
④ 权限表
CREATE TABLE `t_permission`( `id` VARCHAR(32) NOT NULL, `code` VARCHAR(32) NOT NULL, `description` VARCHAR(64) DEFAULT NULL, `url` VARCHAR(128) DEFAULT NULL, PRIMARY KEY(`id`) )ENGINE=InnoDB DEFAULT CHARSET=utf8;
⑤ 角色权限关系表
CREATE TABLE `t_role_permission`( role_id VARCHAR(32) NOT NULL, permission_id VARCHAR(32) NOT NULL, PRIMARY KEY(role_id,permission_id) )ENGINE=InnoDB DEFAULT CHARSET=utf8;
查询SQL:通过userid查权限
SELECT * FROM t_permission WHERE id IN( SELECT role_id FROM t_role_permission WHERE role_id IN( SELECT role_id FROM t_user_role WHERE user_id =? ))
3.2 授权代码
① POJO
@Data public class PessmissionPojo {//权限表 String id; String code; String description; String url; }
② Dao
@Repository public class userDao { @Autowired JdbcTemplate jdbcTemplate; //根据用户ID查询用户权限 public List<String> findPermissionByUserID(String userid){ String SQL = "SELECT * FROM t_permission WHERE id IN(\n"+ "SELECT role_id FROM t_role_permission WHERE role_id IN(\n"+ "SELECT role_id FROM t_user_role WHERE user_id =?\n"+"))"; List<PessmissionPojo> query = jdbcTemplate.query(SQL, new Object[]{userid}, new BeanPropertyRowMapper<PessmissionPojo>(PessmissionPojo.class)); ArrayList<String> permission = new ArrayList<>(); query.forEach((x)->permission.add(x.getCode()); return permission; } }
③ 自定义安全拦截
@Service public class SpringDataUserDetailsService implements UserDetailsService { @Autowired userDao userDao; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { //1.将来连接数据库根据账号查询用户信息 SecurityUserPojo userByUserName = userDao.getUserByUserName(s); if(userByUserName==null){ return null; } //2. 通过用户ID查询权限 List<String> permissionByUserID = userDao.findPermissionByUserID(new StringBuffer().append(userByUserName.getId())); //3. 将permissionByUserID转化成数组 String[] permissions = new String[permissionByUserID.size()]; permissionByUserID.toArray(permissions); //2.登录账号 UserDetails build = User.withUsername(userByUserName.getUsername()).password(userByUserName.getPassword()).authorities(permissions).build(); return build; } }
④ Security配置
@Configuration public class WebSecurityContext extends WebSecurityConfigurerAdapter { //2,密码编码器 @Bean public PasswordEncoder passwordEncoder(){ //不需要对密码进行编码 return NoOpPasswordEncoder.getInstance(); } //3,安全拦截机制 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/wql/a1").hasAuthority("p1")//所有/wql/**的请求必须认证通过 .anyRequest().permitAll()//除了/wql/))其他请求可以访问 .and() .formLogin()//允许表单登录 .loginPage("/loginview") .loginProcessingUrl("/login") .successForwardUrl("/login-success")//自定义登录成功的页面地址 .failureForwardUrl("/login-failure") .permitAll(); http.csrf().disable(); } }
4. 自定义授权方式
Security授权包括两种:
- web授权:通过url拦截进行授权
- 方法授权:通过方法拦截进行授权
底层它们都会调用accessDecisionManager进行授权决策,若为web授权则拦截器为FilterSecurityInterception,若为方法授权则拦截器为MethodSecurityInterceptor,如果同时通过web授权和方法授权则先执行web授权,在执行方法授权,最后决策通过允许访问资源
类关系如下:
4.1 web授权
Web授权在WebSecurityContext配置文件中的安全拦截方法(configure)中配置
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/wql/a1").hasAuthority("p1")//所有/wql/**的请求必须认证通过 .anyRequest().permitAll()//除了/wql/))其他请求可以访问 }
- anyRequest():表示任意请求
- authenticated():保护URL,需要用户登录
- permitAll():指定URL无需保护,一般应用与静态资源文件
- hasRole(String role):限制单个角色访问,角色将被增加"ROLE_"所以"ADMIN"将和"ROLE_ADMIN"进行比较
- hasAuthority(String authority):限制单个权限访问
- hasAnyRole(String……roles):允许多个角色访问
- hasAnyAuthority(String……authorities):允许多个权限访问
- access(String attribute):该方法使用SpEL表达式,可以创建复杂的限制
- haslpAddress(String ipadressExpression):限制IP地址或子网
注:权限的规则的顺序也是非常重要的,更具体的规则应该先写,因为前的规则会覆盖后面的规则,所以在编写权限时需要由小及大
例:权限无效
http.authorizeRequests() .anyRequest().permitAll() .antMatchers("/wql/a1").hasAuthority("p1")
后的"/wql/a1"访问权限p1会被覆盖掉,应该anyRequest().permitAll()允许所有请求
web授权尽量使用基于资源的授权,减少基于角色的授权
4.2 方法授权
从SpringSecurity2.0开始,它支持服务层方法的安全性支持,方法授权是在Controller方法层面进行授权,通过注解的方式
方法授权的三类注解:
- @PreAuthorize
- @PostAuthorize
- @Secured
4.2.1 @Secured
需要在任意@Configuration实例上使用@EnableGlobalMethodSecurity注释来启用基于注解的安全性,以下内容启用SpringSecurity的@Secured注释
@Configuration @EnableGlobalMethodSecurity(securedEnabled = true) public class WebSecurityContext extends WebSecurityConfigurerAdapter { ……………… }
例:
@GetMapping("/helloUser") @Secured({"ROLE_normal","ROLE_admin"}) public String helloUser() { return "hello,user"; }
拥有normal或者admin角色的用户都可以方法helloUser()方法。另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“
假如要设置同时拥有admin & noremal的用户才能方法helloUser()方法,这时候@Secured就无能为力了,需要其他两个注解来完成
4.2.2 @PreAuthorize
Spring的 @PreAuthorize/@PostAuthorize 注解更适合方法级的安全,也支持Spring SpEL表达式语言,提供了基于表达式的访问控制
当@EnableGlobalMethodSecurity(prePostEnabled=true)的时候,@PreAuthorize可以使用:
@Configuration @EnableGlobalMethodSecurity(prePostEnabled= true) public class WebSecurityContext extends WebSecurityConfigurerAdapter { ……………… }
例:拥有normal或者admin角色的用户都可以方法helloUser()方法,此时如果我们要求用户必须同时拥有normal和admin的话,那么可以这么编码
@GetMapping("/helloUser") @PreAuthorize("hasRole('normal') AND hasRole('admin')") public String helloUser() { return "hello,user"; }
4.2.3 @PostAuthorize
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限,Spring EL 提供 返回对象能够在表达式语言中获取返回的对象returnObject
当@EnableGlobalMethodSecurity(prePostEnabled=true)的时候,@PostAuthorize可以使用:
@Configuration @EnableGlobalMethodSecurity(prePostEnabled= true) public class WebSecurityContext extends WebSecurityConfigurerAdapter { ……………… }
例:
@GetMapping("/helloUser") @PostAuthorize(" returnObject!=null && returnObject.username == authentication.name") public User helloUser() { Object pricipal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); User user; if("anonymousUser".equals(pricipal)) { user = null; }else { user = (User) pricipal; } return user; }
注: 这三个最常用也就是@PreAuthorize这个注解了,在使用中主要是配合Spring EL表达式。
5. 会话管理
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。SpringSecurity提供会话管理,认证通过后将身份信息存入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份
5.1 获取会话信息
编写LoginController,实现/wql/a1和/wql/a2的测试资源,并修改success和failure方法
@RestController public class logincontroller { @RequestMapping(value = "/login-success", produces = "text/plain;charset=UTF-8") public String success() { String username =getUsername(); return username+"登录成功"; } @RequestMapping(value = "/login-failure", produces = "text/plain;charset=UTF-8") public String failure() { String username =getUsername(); return username+"登录失败"; } //获取用户名 private String getUsername() { //用户名 String username = null; //通过SecurityContextHolder获取认证通过的用户信息Authentication Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); //通过Authentication的用户身份信息 Object principal = authentication.getPrincipal(); if (principal == null) { username = "匿名用户"; } //判断principal是否是UserDetails的子类 if (principal instanceof UserDetails) { //强转 UserDetails userDetails = (UserDetails) principal; //获取username username = userDetails.getUsername(); } return username; } }
通过SecurityContextHolder.getContext().getAuthentication()获取Authentication,而Authentication就保存权限信息、细节信息、身份信息等
测试:
Comments | NOTHING
Warning: Undefined variable $return_smiles in /www/wwwroot/wql_luoqin_ltd/wp-content/themes/Sakura/functions.php on line 1109