Spring Security
项目搭建使用流程
1. 搭建项目
-
创建好springboot框架
-
在pom文件导入springsecurity框架的依赖包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
-
编写一个controller类,测试方法
@RestController @RequestMapping("/test") public class DemoController { @GetMapping("/demo") public String demo() { return "欢迎"; } }
-
启动项目,浏览器访问当前项目地址:http://localhost:8080/test/demo
访问网址后会跳转到一个默认的登录页面
账号:user 密码会在控制台中随机生成出来
2. 设置登录的用户名和密码
-
第一种方法:通过配置文件
server: port: 9999 spring: security: user: name: admin password: admin
-
第二种方式:通过配置类
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { /* 将账号和加密后的密码放入seciruty的容器中 passwordEncoder:指定加密方式(MD5,BCrypt ) withUser:账号 password:加密后的密码 roles:权限(角色) */ authenticationManagerBuilder.inMemoryAuthentication() .passwordEncoder(new BCryptPasswordEncoder()) .withUser("admin") .password(new BCryptPasswordEncoder().encode("123")) .roles("admin"); } }\
-
第三种方式:自定义编写实现类
-
创建配置类,设置使用哪个userDetailsService实现类
@Configuration public class SecurityTestConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()); } }
-
编写实现类,返回User对象,User对象有用户名密码和操作权限
@Service("userDetailsService") //注入名称必须是userDetailsService public class MyUserDetailsService implements UserDetailsService { //在下面的方法执行查询数据库,判断用户密码的操作 @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); return new User("admin", new BCryptPasswordEncoder().encode("123"), authorities); } }
-
3. 查询数据库完成用户认证
- 导入mybatis-plus和mysql依赖包
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
- 创建数据库和表结构
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(11) NOT NULL COMMENT 'id主键',
`user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
`pass_word` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
- 创建对应的实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users {
private Integer id;
private String userName;
private String passWord;
}
- 整合mybatis-plus,创建接口(mapper),继承mybatis-plus的接口
@Mapper
public interface UserMapper extends BaseMapper<Users> {
}
- 在MyUserDetailsService类调用mapper里面的方法查询数据库进行用户认证
@Service("userDetailsService") //注入名称必须是userDetailsService
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
//在下面的方法执行查询数据库,判断用户密码的操作
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//调用userMapper方法查询数据库
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("user_name", userName);
Users users = userMapper.selectOne(wrapper);
//判断数据库是否有查询的用户名
if (users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
//权限,不能为空
List<GrantedAuthority> authorities =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
//得到数据库查询的账号密码,返回
return new User(users.getUserName(),
new BCryptPasswordEncoder().encode(users.getPassWord()),
authorities);
}
}
- 在启动类添加注解@MapperScan扫描mapper文件夹
@MapperScan("springsecurt.mapper")
@SpringBootApplication
public class SpringsecurtApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurtApplication.class, args);
}
}
- 配置数据库信息
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
4. 自定义配置信息
-
在配置类(SecurityTestConfig类)实现相关配置
@Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.formLogin() //自定义自己编写的登录页面 .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/user/login") //登录访问路径 .defaultSuccessUrl("/test/hello") //认证成功跳转访问路径 .and().authorizeRequests() .antMatchers("/login.html", "/test/hello", "/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证 .anyRequest().authenticated() .and().csrf().disable(); //关闭csrf防护 }
-
创建出相关的页面,controller
login.html页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录</title> </head> <body> <form action="/user/login" method="post"> 用户名:<input type="text" name="username"/> <br> 密 码:<input type="text" name="password"> <br> <input type="submit" value="登录"> </form> </body> </html>
(DemoController类)test/hello 请求方法
@RequestMapping("/hello") public String hello() { return "hello"; }
访问http://localhost:9999/test/hello,正常访问,返回数据,这是前面已经把这个请求地址的权限公开了
访问一个普通的地址http://localhost:9999/test/demo,会拦截并跳转到自己配置的登录页面
输入数据库正确的账号密码后,正常跳转到要访问的地址
5. 角色或权限控制
如果当前的主体具有指定的权限,则返回true,否则返回false
权限不匹配或无权限访问地址,页面会返回403状态
hasAuthority和hashAnyAuthority的区别:
- hasAuthority只支持一个权限角色(例:管理员)
- hashAnyAuthority则支持多个权限角色(例:管理员,客户,普通用户)
设置了公开请求地址,并同时设置了权限控制必须要登录才行
-
第一种方法:hasAuthority
-
在配置类设置当前访问地址有哪些权限(在SecurityTestConfig类的configure方法里)
//当前登录用户,只有具有admins权限才可以访问这个路径 .antMatchers("/test/hello").hasAuthority("admins")
-
在UserDetailService,把返回User对象设置权限(访问地址时需要的权限字符一致)
-
-
第二种方法:hasAnyAuthority
支持多权限/角色 访问
//当前登录用户,拥有admins和ordinary任意一个权限都可以访问 .antMatchers("/test/hello").hasAnyAuthority("admins","ordinary")
-
第三种方法:haseRole
如果用户具备指定的角色,则返回true,对应的权限字符前缀必须是 ROLE_
.antMatchers("/test/hello").hasRole("sale")
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
-
第四种方法:hasAnyRole
和第二种一样,支持多角色/资源访问,对应的权限字符前缀必须是 ROLE_
6. 自定义无权限页面(403)
-
创建页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>当前是403页面</h1> </body> </html>
-
配置信息
//配置没有权限访问跳转自定义页面 httpSecurity.exceptionHandling().accessDeniedPage("/unauth.html");
7. 注解使用
@Securied
用户具有某个角色,可以访问方法,该注解支持多个角色权限
-
启动类开启注解
@EnableGlobalMethodSecurity(securedEnabled = true)
-
在controller方法上面设置,设置角色
@RequestMapping("/update") @Secured({"ROLE_sale", "ROLE_manager"}) public String update() { return "hello update"; }
-
在MyUserDetailsService设置角色
//权限,不能为空 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
@PreAuthorize
在方法上面加上注解,指定需要访问的角色
@PreAuthorize("hasAnyAuthority('sale')")
在MyUserDetailsService设置对应的角色
//权限,不能为空
List<GrantedAuthority> authorities =
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
@postAuthorize
先执行完当前方法,再进行判断是否有当前方法的权限
@PostAuthorize("hasAnyAuthority('admins')")
@PostFillter
方法返回数据进行过滤
- PostFilter的参数指定了 users对象里面的 userName的值必须是**‘admins1’**
- 返回的数据只返回带有admins1的数据
@RequestMapping("/update")
@PostAuthorize("hasAnyAuthority('admins')")
@PostFilter("filterObject.userName=='admin1'")
public Object update() {
List<Users> list = new ArrayList<>();
list.add(new Users(11,"admin1","123"));
list.add(new Users(22,"admin2","123"));
return list;
}
@PreFilter
对请求参数进行过滤
PreFilter的参数指定了id大于10才可以请求,否则就无权限
@RequestMapping("/update")
@PostAuthorize("hasAnyAuthority('admins')")
@PreFilter("filterObject.id<10")
public Object update(User user) {
return hello;
}
8. 用户注销
-
在配置类添加退出的配置
//退出登录请求路径 httpSecurity.logout().logoutUrl("/logout") .logoutSuccessUrl("/test/hello").permitAll();
-
在页面写上退出的请求地址
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 登录成功 <a href="/logout">退出</a> </body> </html>
-
正常登陆后,进入这个页面,然后点击退出按钮,再去访问其他接口发现需要重新登录,测试完成
9. 自动登陆
思路:登录 -->认证成功 -->surice将tokend写入数据库,并返回浏览器,写入cookie -->下次请求 -->读取浏览器cookie中的token发送到surice -->在数据库查找token,有页面正常访问,无需要登录
实现过程:
-
创建数据库表
表名必须是persistent_logins,字段名也是一个都不能改,springsecurity默认的方法只认这些
DROP TABLE IF EXISTS `persistent_logins`; CREATE TABLE `persistent_logins` ( `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名', `series` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '系列', `token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '令牌', `last_used` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最后的时间' ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-
配置类,注入数据源,配置操作数据库对象
//注入数据源 @Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; }
-
配置类配置自动登陆
.and().rememberMe().tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(60) //设置有效时长 .userDetailsService(userDetailsService)
-
在登录页面添加复选框
name名字必须是remember-me
<input type="checkbox" name="remember-me"/>自动登录
在页面登陆后,数据库就会存入信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RdIRDbdK-1633972776480)(C:\Users\27175\AppData\Roaming\Typora\typora-user-images\image-20211004172927898.png)]
8. csrf,跨站请求伪造
注释掉配置的关闭crsf的防护
// .and().csrf().disable(); //关闭csrf防护
在前端请求加一串代码
补充
-
本技术基于SpringBoot技术完成使用
-
核心功能:认证,授权
-
重量级框架,需要依赖其他组件,需要配置很多信息