本文已参加「新人创作礼」活动,一起开启掘金创作之路
基础使用
- 引入基本依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 写一个基本的controller
@RestController
public class TestController {
@GetMapping("hello")
public String hello() {
return "hello";
}
}
- 启动
- 请求我们的controller
- 发现需要先登录
- 日志中发现我们的密码
- 账号密码的配置
- 日志中看到UserDetailsServiceAutoConfiguration类,getOrDeducePassword方法中看到这个打印语句
- 其中有个SecurityProperties.user实体类,进去就能看到我们的账号密码是如何来的
3.这个类有ConfigurationProperties注解,可以直接在配置文件中进行配置
- 在配置文件中配置
spring.security.user.name=admin spring.security.user.password=123456- 使用代码进行配置
@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; }*/ } - 日志中看到UserDetailsServiceAutoConfiguration类,getOrDeducePassword方法中看到这个打印语句
自定义登陆页面
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);
}
}
}
授权操作
- 配置资源权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//有先后顺序
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
...
}
- 路径匹配符
| 匹配符 | 含义 |
|---|---|
| ** | 匹配多层 |
| * | 匹配一次 |
| ? | 匹配单字符 |
- 角色继承 上面的代码admin账号访问不了user的资源,需要角色继承
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_admin > ROLE_user");
return roleHierarchy;
}
使用数据库管理用户
- 添加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>
- 使用默认的数据模型在org.springframework.security.spring-security-core
org/springframework/security/core/userdetails/jdbc/users.ddl - 修改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;
}
自定义数据库管理用户
- 使用jpa
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
- 实体类
@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;
}
}
- dao层
public interface UserDao extends JpaRepository<User, Long> {
/**
* 通过用户名称查找用户
*
* @param userName
* @return
*/
User findUserByUserName(String userName);
}
- 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;
}
}
- 修改SecurityConfig
@Resource
UserServiceImpl userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
自动登陆
- 添加rememberMe()
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().rememberMe().key("test").and().csrf().disable();
}
- remember-me含义
使用base64解密
test:1649317022274:0ddac169600519a5415862f430585b20即用户名:两周后的时间戳:(用户名:两周后的时间戳:密码:key) - 源码分析:
- AbstractAuthenticationProcessingFilter.doFilter
- UsernamePasswordAuthenticationFilter.attemptAuthentication
- AbstractAuthenticationProcessingFilter.successfulAuthentication
- TokenBasedRememberMeServices.onLoginSuccess