SpringSecurity
springsecurity本质上是一个过滤器链(多个过滤器组成)
过滤器加载
配置DelegatingFilterProxy、在doFilter中调用initDelegate()获取过滤链(FilterChainProxy)对象,在过滤链对象中获取Security的所有过滤器
UserDetailsService
通过数据库查询用户信息
PasswordEncoder
对密码进行加密
设置用户名和密码
通过配置文件
spring:
security:
user:
name: 'xjw'
password: 'xjw'
通过配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String password = passwordEncoder.encode("xjw");
auth.inMemoryAuthentication().withUser("xjw").password(password).roles("admin");
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
通过自定义类去数据库中查询用户名和密码
- 编写配置类
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
- 编写UserDetailsService实现类
创建数据库
CREATE DATABASE demo
CREATE TABLE users(
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(20),
`password` VARCHAR(30)
)ENGINE=INNODB DEFAULT CHARSET=utf8
编写实体类
@Data
public class Users {
private Integer id;
private String username;
private String password;
}
编写mapper
@Repository
public interface UserMapper extends BaseMapper<Users> {
}
编写service
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
Users users = userMapper.selectOne(wrapper);
if (users == null){
throw new UsernameNotFoundException("用户不存在!");
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User(users.getUsername(),passwordEncoder.encode(users.getPassword()),auths);
}
}
自定义登录页面
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //设置登录页面
.loginProcessingUrl("/user/login") //设置登录请求url
.defaultSuccessUrl("/test/index") //登录成功后跳转的url
.permitAll() //以上设置的路径全部放行
.and().authorizeRequests()
.antMatchers("/","/test/hello","/user/login") //设置需要放行的路径
.permitAll().anyRequest().authenticated() //从上往下,除了上面放行的路径,其他的都要认证
.and().csrf().disable(); //关闭防护 解决跨域问题
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
登录表单页面
<!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>
注意
表单的name必须为username和password,因为security底层要求,如果不是,则底层拿不到提交的登录信息
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
//判断请求方式是否为post方式
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//通过obtainUsername()方法获取前台提交的登录用户名
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
//通过obtainPassword()方法获取前台提交的登录密码
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
//this.usernameParameter就是这个类的静态常量属性 = "username"
return request.getParameter(this.usernameParameter);
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
//this.passwordParameter就是这个类的静态常量属性 = "password"
return request.getParameter(this.passwordParameter);
}
}
基于角色或权限进行访问控制
当用户拥有要求的权限或者角色时才可以访问,否则报403错误
hasAuthority()
//当访问/test/index路径时需要当前登录的用户拥有admin权限,否则无法访问
antMatchers("/test/index").hasAuthority("admin")
//如果hasAuthority()中设置了多个权限则要求当前登录的用户同时拥有设置的权限才能进行访问
//如果hasAuthority("admin,user"),则要求当前登录的用户同时拥有admin权限和user权限
hasAnyAuthority()
//当访问/test/index路径时要求当前登录的用户拥有admin或者user权限其中一个即可
antMatchers("/test/index").hasAnyAuthority("admin,user")
hasRole()
//当访问/test/index路径时要求当前登录的用户拥有ROLE_sale用户角色
antMatchers("/test/index").hasRole("sale")
//如果hasRole()中设置了多个权限则要求当前登录的用户同时拥有设置的权限才能进行访问
//如果hasRole("admin,user"),则要求当前登录的用户同时拥有ROLE_admin权限和ROLE_user权限
注意
hasRole()方法的源码
private static String hasRole(String role) { //判断设置的角色为null,如果是抛出异常 Assert.notNull(role, "role cannot be null"); //判断设置的角色是否以"ROLE_"前缀开头,如果是抛出异常 Assert.isTrue(!role.startsWith("ROLE_"), () -> "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'"); //对角色进行拼接“ROLE_”前缀 return "hasRole('ROLE_" + role + "')"; }
角色不能为空
在hasRole()的底层实现中它会默认在这个要求的角色前面加上”ROLE_“前缀,所以当我们给用户设置角色时需要有"ROLE_"前缀
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper<Users> wrapper = new QueryWrapper<>(); wrapper.eq("username",username); Users users = userMapper.selectOne(wrapper); if (users == null){ throw new UsernameNotFoundException("用户不存在!"); } //给用户设置角色时需要以“ROLE_”前缀开头,否则匹配不到对应的角色 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale"); return new User(users.getUsername(),passwordEncoder.encode(users.getPassword()),auths); }
在对路径设置要求的角色时,角色不能以"ROLE_"前缀开头,否则启动项目时会报错
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //自定义自己编写的登录页面 .loginPage("/login.html") //设置登录页面 .loginProcessingUrl("/user/login") //设置登录请求url .defaultSuccessUrl("/test/index") //登录成功后跳转的url .permitAll() //以上设置的路径全部放行 .and().authorizeRequests() .antMatchers("/","/test/hello","/user/login") //设置需要放行的路径 .permitAll() //在给路径设置需要的角色时不能以前缀“ROLE_”开头,否则启动项目会报错 .antMatchers("/test/index").hasRole("ROLE_sale") .anyRequest().authenticated() //从上往下,除了上面放行的路径,其他的都要认证 .and().csrf().disable(); //关闭防护 解决跨域问题 }
hasAnyRole()
//当访问"/test/index"路径时需要当前登录的用户拥有“ROLE_sale”角色或者“ROLE_admin”角色其中一个
antMatchers("/test/index").hasAnyRole("sale,admin")
注意
hasAnyRole()源码
private static String hasAnyRole(String... authorities) { String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','ROLE_"); return "hasAnyRole('ROLE_" + anyAuthorities + "')"; }
hasAnyRole()方法在传递参数时第一个角色带"ROLE_"前缀时启动项目不会报错,但是在会找不到对应的角色,因为底层代码默认在原来的基础之上再加了一个"ROLE_"前缀,当然,如果想找到对应的角色,那么可以给当前登录用户设置角色时前面再加一个“ROLE_”前缀,一般不建议这么做
从设置第二个角色开始可以带“ROLE_”前缀也可以不带,具体的匹配规则看源码
处理没有没有权限或者角色
跳转到自定义403页面
在配置类中的configure(HttpSecurity http)方法中配置
//当没有权限时跳转到指定的页面
http.exceptionHandling().accessDeniedPage("/403.html");
常用注解
@Secured
使用前需要在启动类或者配置类中开启注解,否则注解不起作用
@EnableGlobalMethodSecurity(securedEnabled = true)
注解作用是访问对应的方法需要拥有指定的角色
@GetMapping("/update")
@Secured("ROLE_admin1")
public String update(){
return "hello update";
}
当访问/update时当前登录的用户需要有"ROLE_admin"角色,否则无权限
可以放多个角色,要求登录的用户有其中一个角色就可以
@GetMapping("/update")
@Secured({"ROLE_admin","ROLE_sale"})
public String update(){
return "hello update";
}
@PreAuthoriza
在使用注解之前需要在配置类或者启动类中开启注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
在执行方法之前进行权限或者角色身份校验
//在执行方法之前校验当前登录的用户是否同时拥有指定的权限
@PreAuthorize("hasAuthority('admin')")
@GetMapping("/update")
public String update(){
return "hello update";
}
//在执行方法之前校验当前登录的用户是否拥有admin权限和sale权限
@PreAuthorize("hasAuthority('admin') AND hasAuthority('sale')")
@GetMapping("/update")
public String update(){
return "hello update";
}
//在执行方法之前校验当前登录用户是否拥有指定权限中的任意一个或者多个
@PreAuthorize("hasAnyAuthority('admin,sale')")
@GetMapping("/update")
public String update(){
return "hello update";
}
//在执行方法之前校验当前登录的用户是否为admin角色
@PreAuthorize("hasRole('admin')")
@GetMapping("/update")
public String update(){
return "hello update";
}
//在执行方法之前校验当前登录的用户是否同时为"ROLE_admin"角色和"ROLE_sale"角色
@PreAuthorize("hasRole('admin') AND hasRole('sale')")
@GetMapping("/update")
public String update(){
return "hello update";
}
//在执行方法前校验当前登录的用户是否为"ROLE_sale"角色或者"ROLE_admin"
@PreAuthorize("hasAnyRole('sale,ROLE_admin')")
@GetMapping("/update")
public String update(){
return "hello update";
}
@PostAuthoriza
在使用注解前需要在配置类中或者启动类中开启注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
在执行完方法后再去校验身份(在return语句前进行校验)
//在返回结果之前校验当前登录的用户是否为"ROLE_sale"角色或者"ROLE_admin"角色
@PostAuthorize("hasAnyRole('sale,ROLE_admin')")
@GetMapping("/update")
public String update(){
return "hello update";
}
用法和@PreAuthorize一样,只是校验的时机不同
@PostFilter
对返回值进行过滤
@PostAuthorize("hasAnyRole('sale,ROLE_admin')")
@PostFilter("filterObject.username == 'admin'") //过滤返回值,只能返回username为"admin"
@GetMapping("/update")
public List<Users> update(){
List<Users> users = new ArrayList<>();
users.add(new Users(1,"admin","123"));
users.add(new Users(1,"admin1","123"));
users.add(new Users(1,"admin2","123"));
return users;
}
注意
@PostFilter(“filterObject.username == ‘admin’”) 需要判断的属性需要和返回的实体类中的属性名一致,否则会报错
针对集合,返回值需要为集合
@PreFilter
对方法传递的参数进行过滤
//要求ids几个中的数据能被2整除
@PreFilter(filterTarget="ids", value="filterObject%2==0")
public void delete(List<Integer> ids, List<String> usernames) {
System.out.println(ids)
}
用户注销
在配置类中设置
protected void configure(HttpSecurity http) throws Exception {
//退出,采用跳转页面,也可以用退出处理器来处理退出后的操作
http.logout().logoutUrl("/logout").logoutSuccessUrl("successful.html").permitAll();
}
实现自动登录
创建数据库表
CREATE TABLE persistent_logins(
`username` VARCHAR(64) NOT NULL,
`series` VARCHAR(64) NOT NULL,
`token` VARCHAR(64) NOT NULL,
`last_used` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY(`series`)
)ENGINE=INNODB DEFAULT CHARSET=utf8;
注意
表名指定为persistent_logins,因为底层的默认的sql语句就是用的persistent_logins这个表名
编写配置类
//往容器中注入persistentTokenRepository对象
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true); 设置是否在启动时创建表
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.and().rememberMe().tokenRepository(persistentTokenRepository)
.tokenValiditySeconds(60*60) //设置有效时间
.userDetailsService(userDetailsService) //设置查询用户信息的Service
}
编写前台页面
<!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="checkbox" name="remember-me">自动登录<br/>
<input type="submit" value="登录">
</form>
</body>
</html>
要求自动登录的name必须为"remember-me",因为底层代码是根据这个name来获取状态的
CSRF
跨站请求伪造(英语:Cross-site request forgery)