这篇文章源自我最近在做的一个需求,leader希望能做一个接口监控页,而这个页面需要做登录拦截。由于我比较懒,不想手写拦截器来做登录认证,而之前又没有玩过Spring Security
,于是就把它引入进来了。
本文基于SpringBoot 2.2.6
,希望这篇文章能够帮助你快速上手SpringBoot Security
。
增加pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
增加此依赖后再启动项目,你应该能看到以下页面:
如果你没做任何配置,默认的用户名是user
,密码会打印在控制台上。你也可以在配置文件中增加以下配置用来显示的指定用户名和密码:
spring:
security:
user:
name: user
password: 123456
我们知道,我们平常写拦截器的时候都要手动地解除静态资源
的拦截,SpringBoot Securiry
也是如此,你需要增加以下配置来解除拦截:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.ignoring()
.antMatchers("/static/**");
}
}
至此,我以为一切都结束了,拦截器不用写了,登录过后页面之间都能正常跳转。正当我美滋滋地以为一切大功告成的时候发现:ajax请求不了了。无论我以什么姿势发送请求到后端,都会报405错误。
于是我上网去搜相关资料,发现原因是:Spring Security
默认会对csrf
做拦截,若想对ajax解除拦截,就得在配置项中禁用此项功能。
实现授权逻辑
首先需要创建最基础的三张表:用户(user)
、角色(role)
以及用户角色关联表(user_role)
,表结构如下:
user表
CREATE TABLE `ba_user` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`NAME` varchar(32) DEFAULT NULL COMMENT '用户名',
`PASSWORD` varchar(255) DEFAULT NULL COMMENT '密码',
`IS_LOCKED` tinyint(1) DEFAULT '0' COMMENT '是否有效',
`IS_EXPIRED` tinyint(1) DEFAULT '0' COMMENT '是否过期',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
role表
CREATE TABLE `ba_role` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`ROLE_NAME` varchar(32) DEFAULT NULL COMMENT '角色名称',
`IS_VALIDATE` tinyint(1) DEFAULT '0' COMMENT '是否有效',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
user_role表
CREATE TABLE `user_role` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`USER_ID` int(11) NOT NULL COMMENT '用户ID',
`ROLE_ID` int(11) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
实体类实现UserDetails
接口
在实体类user
中去实现一个接口UserDetails
,这个接口是由Security
提供的:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "ba_user")
public class BaUser implements UserDetails {
/**
* 主键
*/
@Id
@Column(name = "ID")
@GeneratedValue(generator = "JDBC")
private Integer id;
/**
* 用户名
*/
@Column(name = "`NAME`")
private String name;
/**
* 密码
*/
@Column(name = "`PASSWORD`")
private String password;
/**
* 角色列表
*/
private List<BaRole> roles;
/**
* 是否有效
*/
@Column(name = "IS_LOCKED")
private Boolean isLocked;
/**
* 是否过期
*/
@Column(name = "IS_EXPIRED")
private Boolean isExpired;
public static BaUserBuilder builder() {
return new BaUserBuilder();
}
/**
* security底层会调用该方法来获取登录用户的角色信息
* 实现逻辑根据自己的需求来写
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority>authorities=new ArrayList<>(roles.size());
for (BaRole role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
return authorities;
}
@Override
public String getUsername() {
return this.name;
}
@Override
public boolean isAccountNonExpired() {
return !this.isExpired;
}
@Override
public boolean isAccountNonLocked() {
return !this.isLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return !this.isLocked;
}
@Override
public boolean isEnabled() {
return true;
}
}
实现查询用户逻辑
Security
底层帮我们做了用户认证,但是根据什么规则来进行用户查询的逻辑还是要自己实现的,比如我们需要根据用户名
查询账号信息:
定义一个接口,该接口继承UserDetailsService
,也是由Security
提供的:
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService {
}
实现类:
@Service
@Transactional(readOnly = true)
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private BaUserMapper userMapper;
@Autowired
private BaRoleMapper roleMapper;
@Autowired
private UserRoleMapper userRoleMapper;
/**
* 根据用户名查询用户信息
* @param s 用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Condition condition = new Condition(BaUser.class);
// 根据登录表单的用户名进行查询,并且账号的状态是有效的,我们一般定义0为"非" 1为"是"
// 所以这段逻辑是查询"没被锁"的并且"没过期"的账号
condition.createCriteria()
.andEqualTo("name", s)
.andEqualTo("isLocked", 0)
.andEqualTo("isExpired", 0);
List<BaUser> list = userMapper.selectByCondition(condition);
if (list.size() <= 0) {
throw new UsernameNotFoundException("用户不存在");
}
// 查询登录用户的角色
BaUser user = list.get(0);
Condition condition2 = new Condition(UserRole.class);
condition2.createCriteria()
.andEqualTo("userId", user.getId());
// 拿到关联的角色列表
List<UserRole> roles = userRoleMapper.selectByCondition(condition2);
List<Integer> roleIds = Lists.newArrayList();
if (roles.size() > 0) {
roles.forEach( userRole -> {
roleIds.add(userRole.getRoleId());
});
}
// 获取角色名
Condition condition3 = new Condition(BaRole.class);
condition3.createCriteria()
.andEqualTo("isValidate", 0)
.andIn("id", roleIds);
user.setRoles(roleMapper.selectByCondition(condition3));
return user;
}
}
配置SpringSecurity
之前我们在配置类中只配置了静态资源的放行,现在我们多做几个配置:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 将我们刚刚实现的service注入进来供security调用
@Autowired
private UserService userService;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.ignoring()
.antMatchers("/static/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 这句话代表登录过后就放行所有请求
.anyRequest().authenticated()
// 这句话代表表单登录以及登陆页面的路径,我用的是security自带的登录页
.and().formLogin()
.loginProcessingUrl("/login")
// 这里的username和password要和登录表单的<name>属性一致,否则security找不到
.usernameParameter("username").passwordParameter("password").permitAll()
// 登出后的处理,security默认的登出路径是/logout
.and().logout().permitAll()
// 关闭csrf ajax请求405的原因也在于此
.and().csrf().disable();
}
}
创建一个用户
在user
表中插入一条记录:
密码可以直接跑个单元测试生成:
@Test
public void passTest() {
String encode = new BCryptPasswordEncoder().encode("123456");
System.out.println(encode);
}
大功告成
如果配置到这里,你的程序应该就能够正常运行了。这里只给出了SpringBoot Security
最基础的配置,目的是为了让程序快速跑起来看效果,之后复杂的配置再根据需求一点点的配就是了。比如网上很多配置都是长这样的:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
/**
* @param charSequence 明文
* @param s 密文
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()));
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("超级管理员")
.anyRequest().authenticated()//其他的路径都是登录后即可访问
.and().formLogin().loginPage("/login_page").successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("{\"status\":\"ok\",\"msg\":\"登录成功\"}");
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"登录失败\"}");
out.flush();
out.close();
}
}).loginProcessingUrl("/login")
.usernameParameter("username").passwordParameter("password").permitAll()
.and().logout().permitAll().and().csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/reg");
}
}
上面的配置主要增加了权限
的路径访问控制以及对登录成功
及登录失败
之后要做的业务逻辑,这些逻辑对于我的项目来说暂不需要。
以最小知识原则去快速运用一项技术,要比一开始就把全部知识弄明白要更容易让人接受。