这篇文章源自我最近在做的一个需求,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");
}
}
上面的配置主要增加了权限的路径访问控制以及对登录成功及登录失败之后要做的业务逻辑,这些逻辑对于我的项目来说暂不需要。
以最小知识原则去快速运用一项技术,要比一开始就把全部知识弄明白要更容易让人接受。
本文介绍如何在SpringBoot项目中快速集成SpringSecurity,包括配置登录拦截、解决Ajax请求405错误、实现授权逻辑等关键步骤。
830

被折叠的 条评论
为什么被折叠?



