Spring Security学习

223 阅读5分钟

本文已参加「新人创作礼」活动,一起开启掘金创作之路

基础使用

  1. 引入基本依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. 写一个基本的controller
@RestController
public class TestController {
	@GetMapping("hello")
	public String hello() {
		return "hello";
	}
}
  1. 启动
  2. 请求我们的controller
  3. 发现需要先登录
  4. 日志中发现我们的密码
  5. 账号密码的配置
    1. 日志中看到UserDetailsServiceAutoConfiguration类,getOrDeducePassword方法中看到这个打印语句
    2. 其中有个SecurityProperties.user实体类,进去就能看到我们的账号密码是如何来的 3.这个类有ConfigurationProperties注解,可以直接在配置文件中进行配置
    3. 在配置文件中配置
    spring.security.user.name=admin
    spring.security.user.password=123456
    
    1. 使用代码进行配置
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    	@Bean
    	PasswordEncoder passwordEncoder() {
    		//不加密
    		return NoOpPasswordEncoder.getInstance();
    	}
    
    	@Override
    	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    		//两个用户,中间使用and相连
    		auth.inMemoryAuthentication().withUser("jaminye").password("123456").roles("admin")
    				.and().withUser("user").password("12354").roles("user");
    	}
    	//作用同上
    	/*@Override
        @Bean
        protected UserDetailsService userDetailsService() {
        	// 内存
        	InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        	manager.createUser(User.withUsername("jaminye").password("123456").roles("admin").build());
        	manager.createUser(User.withUsername("user").password("12354").roles("user").build());
        	return manager;
        }*/
    }    
    

自定义登陆页面

1. 重写两个方法
```java
@Override
protected void configure(HttpSecurity http) throws Exception {
	//指定登陆页面
	http.authorizeRequests().anyRequest().authenticated()
			.and()
			.formLogin()
			//登陆页面配置
			.loginPage("/login.html")
			//默认与loginPage相同 详情见源码FormLoginConfigurer.init super.init updateAuthenticationDefaults
			.loginProcessingUrl("/doLogin")
			//默认username,password, 详情见源码FormLoginConfigurer的构造函数
			.usernameParameter("name")
			.passwordParameter("passwd")
			//上方接口不需要登陆验证
			//第二个参数不设置默认false,登陆后返回来源页面,第二个参数设置为true同successForwardUrl始终返回设置的页面,
			// 但是successForwardUrl 是转发,defaultSuccessUrl重定向
			// .defaultSuccessUrl("/index",true)
			.successForwardUrl("/index")
			.permitAll()
			.and()
			.logout()
			//注销登陆的url,默认logout
			.logoutUrl("/logoutUrl")
			//与logout类似,可指定请求方式
			// .logoutRequestMatcher(new AntPathRequestMatcher("/logoutUrl", "POST"))
			// 注销成功后跳转地址
			.logoutSuccessUrl("/index")
			//清楚cookie
			.deleteCookies()
			//清除cookie和使session失效,默认就会执行
			.clearAuthentication(true)
			.invalidateHttpSession(true)
			.permitAll()
			.and()
			.csrf().disable();
}
```
2. 表单post方式提交到/login.html,username,password不要错
```html
    <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" type="text/css" href="css/main.css"/>
</head>
<body>
<form action="/doLogin" method="post">
    <div class="div-margin">
        <label for="name">用户名</label>
        <input type="text" name="name" id="name"/>
    </div>
    <div>
        <label class="label-margin" for="pass">密码</label>
        <input class="input-margin" type="password" name="passwd" id="pass"/>
    </div>
    <div>
        <button type="submit">
            <span>登陆</span>
        </button>
    </div>
</form>
<div>
    <input type="button" onclick="window.location.href='/logoutUrl'" value="注销">
</div>
</body>
<script src="js/main.js"></script>
</html>
```

使用json格式交互()

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Bean
	PasswordEncoder passwordEncoder() {
		//不加密
		return NoOpPasswordEncoder.getInstance();
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		//两个用户,中间使用and相连
		auth.inMemoryAuthentication().withUser("jaminye").password("123456").roles("admin")
				.and().withUser("user").password("12354").roles("user");
	}

	@Override
	public void configure(WebSecurity web) throws Exception {
		//不拦截静态资源
		web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		//指定登陆页面
		http.authorizeRequests().anyRequest().authenticated()
				.and()
				.formLogin()
				//登陆页面配置
				.loginPage("/login.html")
				//默认与loginPage相同 详情见源码FormLoginConfigurer.init super.init updateAuthenticationDefaults
				.loginProcessingUrl("/doLogin")
				//默认username,password, 详情见源码FormLoginConfigurer的构造函数
				// 自定义这里没用了
				.usernameParameter("name")
				.passwordParameter("passwd")
				//上方接口不需要登陆验证
				//第二个参数不设置默认false,登陆后返回来源页面,第二个参数设置为true同successForwardUrl始终返回设置的页面,
				// 但是successForwardUrl 是转发,defaultSuccessUrl重定向
				// .defaultSuccessUrl("/index",true)
				.successForwardUrl("/index")
				.permitAll()
				.and()
				.logout()
				//注销登陆的url,默认logout
				.logoutUrl("/logoutUrl")
				//与logout类似,可指定请求方式
				// .logoutRequestMatcher(new AntPathRequestMatcher("/logoutUrl", "POST"))
				// 注销成功后跳转地址
				.logoutSuccessUrl("/index")
				//清楚cookie
				.deleteCookies()
				//清除cookie和使session失效,默认就会执行
				.clearAuthentication(true)
				.invalidateHttpSession(true)
				.permitAll()
				.and()
				.csrf().disable();
		//添加过滤器
		http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
	}


	@Bean
	LoginFilter loginFilter() throws Exception {
		LoginFilter loginFilter = new LoginFilter();
		//成功
		loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
			response.setContentType("application/json; charset=UTF-8");
			PrintWriter out = response.getWriter();
			User user = (User) authentication.getPrincipal();
			Map<String, Object> result = new HashMap<>(8);
			result.put("code", 0);
			result.put("msg", user.getUsername() + "登陆成功");
			out.write(new ObjectMapper().writeValueAsString(result));
			out.flush();
			out.close();
		});
		//失败根据异常返回
		loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
			response.setContentType("application/json; charset=UTF-8");
			PrintWriter out = response.getWriter();
			Map<String, Object> result = new HashMap<>(8);
			if (exception instanceof LockedException) {
				result.put("code", 100);
				result.put("msg", "账户被锁");
			} else if (exception instanceof CredentialsExpiredException) {
				result.put("code", 200);
				result.put("msg", "密码过期");
			} else if (exception instanceof AccountExpiredException) {
				result.put("code", 300);
				result.put("msg", "账户过期");
			} else if (exception instanceof DisabledException) {
				result.put("code", 400);
				result.put("msg", "账户禁用");
			} else if (exception instanceof BadCredentialsException) {
				result.put("code", 500);
				result.put("msg", "用户密码错误");
			} else {
				result.put("code", 600);
				result.put("msg", "登陆失败");
			}
			out.write(new ObjectMapper().writeValueAsString(result));
			out.flush();
			out.close();
		});
		loginFilter.setAuthenticationManager(authenticationManagerBean());
		loginFilter.setFilterProcessesUrl("/doLogin");
		return loginFilter;
	}
}
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

	public LoginFilter() {
		this.setUsernameParameter("name");
		this.setPasswordParameter("passwd");
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		// 校验请求方式
		if (!"POST".equals(request.getMethod())) {
			throw new AuthenticationServiceException("method is not supported");
		}
		//json格式
		if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
			//存储登陆信息
			Map<String, String> loginData = new HashMap<>(8);
			try {
				//	获取登陆信息
				loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
			} catch (IOException e) {
				throw new AuthenticationServiceException("get loginData not failed");
			}

			String userName = loginData.get(getUsernameParameter());
			String password = loginData.get(getPasswordParameter());
			if (Objects.isNull(userName)) {
				userName = "";
			}
			if (Objects.isNull(password)) {
				password = "";
			}
			userName = userName.trim();
			UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password);
			setDetails(request, authRequest);
			return this.getAuthenticationManager().authenticate(authRequest);
		} else {
			return super.attemptAuthentication(request, response);
		}
	}
}

授权操作

  1. 配置资源权限
@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests()
			//有先后顺序
			.antMatchers("/admin/**").hasRole("admin")
			.antMatchers("/user/**").hasRole("user")
			.anyRequest().authenticated()
			...
}
  1. 路径匹配符
匹配符含义
**匹配多层
*匹配一次
?匹配单字符
  1. 角色继承 上面的代码admin账号访问不了user的资源,需要角色继承
@Bean
RoleHierarchy roleHierarchy() {
	RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
	roleHierarchy.setHierarchy("ROLE_admin > ROLE_user");
	return roleHierarchy;
}

使用数据库管理用户

  1. 添加jdbc和mysql连接依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
  1. 使用默认的数据模型在org.springframework.security.spring-security-core org/springframework/security/core/userdetails/jdbc/users.ddl
  2. 修改userDetailsService使用数据库
@Override
@Bean
protected UserDetailsService userDetailsService() {
    JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
    //初始化数据
    if (!manager.userExists("jaminye")) {
    	manager.createUser(User.withUsername("jaminye").password("123456").roles("admin").build());
    }
    if (!manager.userExists("user")) {
    	manager.createUser(User.withUsername("user").password("12354").roles("user").build());
    }
    return manager;
}		

自定义数据库管理用户

  1. 使用jpa
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
  1. 实体类
@Entity
public class User implements UserDetails {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String username;
	private String password;
	/**
	 * 账户是否过期
	 */
	private boolean accountNonExpired;
	/**
	 * 账户是否锁定
	 */
	private boolean accountNonLocked;
	/**
	 * 密码是否没有过期
	 */
	private boolean credentialsNonExpired;
	/**
	 * 账户是否可用
	 */
	private boolean enabled;
	/**
	 * FetchType.EAGER  及时获取数据
	 * CascadeType.PERSIST   保存user时有roles属性也会向数据库插入Role
	 */
	@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
	private List<Role> roles;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
		getRoles().stream().map(role -> authorities.add(new SimpleGrantedAuthority(role.getName())));
		return authorities;
	}

	@Override
	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	@Override
	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	@Override
	public boolean isAccountNonExpired() {
		return accountNonExpired;
	}

	public void setAccountNonExpired(boolean accountNonExpired) {
		this.accountNonExpired = accountNonExpired;
	}

	@Override
	public boolean isAccountNonLocked() {
		return accountNonLocked;
	}

	public void setAccountNonLocked(boolean accountNonLocked) {
		this.accountNonLocked = accountNonLocked;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return credentialsNonExpired;
	}

	public void setCredentialsNonExpired(boolean credentialsNonExpired) {
		this.credentialsNonExpired = credentialsNonExpired;
	}

	@Override
	public boolean isEnabled() {
		return enabled;
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public List<Role> getRoles() {
		return roles;
	}

	public void setRoles(List<Role> roles) {
		this.roles = roles;
	}
}
@Entity(name = "t_role")
public class Role {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	private String name;
	private String nameZh;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getNameZh() {
		return nameZh;
	}

	public void setNameZh(String nameZh) {
		this.nameZh = nameZh;
	}
}
  1. dao层
public interface UserDao extends JpaRepository<User, Long> {

	/**
	 * 通过用户名称查找用户
	 *
	 * @param userName
	 * @return
	 */
	User findUserByUserName(String userName);
}
  1. service层
public class UserServiceImpl implements UserDetailsService {

	@Resource
	UserDao userDao;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userDao.findUserByUserName(username);
		if (Objects.isNull(user)) {
			throw new UsernameNotFoundException("用户不存在");
		}
		return user;
	}
}
  1. 修改SecurityConfig
@Resource
UserServiceImpl userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	auth.userDetailsService(userService);
}

自动登陆

  1. 添加rememberMe()
@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().rememberMe().key("test").and().csrf().disable();
}
  1. remember-me含义 使用base64解密test:1649317022274:0ddac169600519a5415862f430585b20用户名:两周后的时间戳:(用户名:两周后的时间戳:密码:key)
  2. 源码分析:
    1. AbstractAuthenticationProcessingFilter.doFilter
    2. UsernamePasswordAuthenticationFilter.attemptAuthentication
    3. AbstractAuthenticationProcessingFilter.successfulAuthentication
    4. TokenBasedRememberMeServices.onLoginSuccess