废话

这篇文章是用 security 时写的,当时是边用边学,很多地方都没搞得很清楚,趁热打铁写了这篇,不过没有写完整。后来居然忘了补全,哎!我真是…

Spring Boot 与 Spring Security

前言

首先,先谈谈为什么要使用 Spring Security。

对于一样陌生的东西,学习的最佳方式就是把它引入到你要解决的问题。知道了这是用于解决什么问题的,就差不多晓得了这玩意的大致工作原理。

我就比较蠢了,契机只是我想实现一个登录注册的功能,刚好在学习的书本上看到 Security ,就按着书本来使用了。非常糟糕的一点是书上谈的非常浅,扩展性不强,我为了扩展,就又傻了吧唧花了一堆时间学习怎么把它扩展起来。

途中看网上的资料,发现用的更多的是 Shiro,而且普遍都说 Shiro 已经足够用了,我因为已经在路上了,就决定先把这边搞定…

先把最基本的设置弄出来,看它解决了什么问题,就能明白我上面所说的扩展性不强了。

不过在这之前,还是先看看 Security 到底是什么,以及它解决了什么问题。

解释

我就按自己的理解说了。

以前写 javaee 代码时,针对 “访问权限” 的控制都是通过写 filter ,即过滤器。

为什么要搞这个是很好理解的,普通的会员肯定不能访问管理界面,所以需要做权限控制。

PS:现在大部分的路由控制都交给前端来做了,不过后端也需要做一些验证。

最基本的设置

先把最基本的设置弄出来,看它解决了什么问题,就能明白我上面所说的扩展性不强了。

以下都是基于 JavaConfig 的配置。

指定 Web 安全的细节,可以通过建立一个继承自 WebSecurityConfigurerAdapter 的配置类,然后重载 WebSecurityConfigurerAdapter 的一个或多个方法来实现。

这个配置类放哪都可以,不过最好还是建立一个 config 包来专门存放配置类(这些细节暂且不提)。

下面这段代码,前面两个注解分别是声明这个类是配置类,以及启用 Spring Security。

把 Spring Security 引入项目后,Spring MVC Security 应该就已经自动开启了。因为我引入后,没做任何操作,项目的所有路径(访问网页资源)就已经需要验证登录了。

所以第二个注解的意思应该是忽略默认 Security 配置,然后用该 java config 来配置 Security。

第一个传入 HttpSecurity 参数的 configure 方法是配置如何通过拦截器保护请求。

默认的 configure(HttpSecurity http) (WebSecurityConfigurerAdapter 里实现的方法)里就是下面代码里的内容。

由此看见,默认配置就是对所有的请求都需要认证…

所以产生了上面那种情况。

下面传入 AuthenticationManagerBuilder 参数 的 configure 方法是用于配置认证的。

在这里配置的就是基于内存用户储存的一个认证。

认证方式有基于内存用户储存基于数据库表。从字面就能理解,基于内存就是我下面的这种,把用户的信息写死了。基于数据库表就是把前端从登录页面传来的信息与数据库里的信息匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
authorizeRequests()
.anyRequest().authenticated()//对于任意请求,允许认证过的用户访问。
.and()
.formLogin()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.
inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}

修改登录页面

有了上面的基础设置,任何页面的 url 都会跳转到一个默认的登录界面,如果想要换成自己写的绚丽的页面该怎么搞呢?

修改传入 HttpSecurity 参数的 configure 方法即可。

1
2
.formLogin()
.loginPage("/login.html").permitAll()//设定登录页面为 login.html ,并允许所有人访问。

我们自己设置的表单某些地方可能会和 Security 默认的不一样。

比如 Security 获取前端传来的用户名和密码,name 属性必须为 username,password。

Security 也可以指定取值的方式

1
2
.usernameParameter("username")
.passwordParameter("password");//前端表单的用户名与密码的 name属性值。

审查默认登录页面的 form 就会发现,表单的 action 值为 “login”,我们可以在 Security 设置处理表单的 url。

1
.loginProcessingUrl("/login")//登陆提交的处理url意思是让这个url(通常是action)来处理表单

当不用这个方法的时候,效果和上面这个方法是一样的,即把表单交给 /login 处理。这个处理是 Security 内置的处理。

如果改为

1
.loginProcessingUrl("/myLogin")//登陆提交的处理url意思是让这个url(通常是action)来处理表单

默认登录页面的 form 的 action 也会相应改为 /myLogin。

登录页面重定向的地址和表单提交的地址务必一致!

要想使用 Security 来帮我们处理登录验证,就把 form 的 action 设为 /login 就好了。

为了避免 Security 和 Spring MVC 冲突,如果之前写了处理 /login 的 Controller ,需要把 Controller 里改一下。否则会冲突, /login 会优先被 Spring MVC 处理,就不能达到验证的效果了。

我今天遇到类似这样的问题, /login 一直没有被 Security 捕捉,百度到上面这个可能发生冲突的原因,删除 out 文件夹,重新跑,就成功了。

建立自己的登录角色

从网页输入自己的信息登录。之前我的实现方式是把前端的信息封装为一个 User 对象,然后把 User 的属性(name,password)和数据库交互,判断是否符合。

Security 是通过传来的 username 取数据库找到对应的角色对象,然后比较该对象与前端传来的用户名和密码是否符合。

我们自己定一个 Login 类,里面储存了登陆的信息,

UserDetailsService的职责非常简单:给一个用户名,返回一个UserDetails 实现,UserDetails会回答这些问题:用户的合法性、用户名/密码、用户的权利(org.springframework.security.core.GrantedAuthority),接下来的事情继续交给SpringBoot处理。

3.1 创建UserDetails的实现类

为了使得我们的用户角色类能和security中的能够结合起来,需要重新建一个类MyUserDetails实现UserDetails接口。

默认的 User 只有 username 和 password 两个属性。

自己可以继承 UserDatails 来实现自己的 User 类。

下面的代码值得解释的有

getAuthorities() 这个方法返回用户的权限。

比如一个网站的角色有很多,vip1,vip2…

它们都具有“骂人”的权限。

我这里默认设置了权限为 eat。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Login implements UserDetails{
@NotNull
private String name;
@NotNull
private String email;
@NotNull
private String password;

public void setUsername(String username) { this.name = username;}
public void setRole(String role) {this.role = role;}
public void setEmail(String email) {this.email = email;}
public void setPassword(String password) {this.password = password;}
public String getRole() {return role;}
public String getEmail() {return email;}
public String getPassword() {return password;}
public String getUsername() {return name;}
public boolean isAccountNonExpired() {return true;}
public boolean isAccountNonLocked() {return true;}
public boolean isCredentialsNonExpired() {return true;}
public boolean isEnabled() {return true;}

public Collection<? extends GrantedAuthority> getAuthorities() {
// 根据自定义逻辑来返回用户权限,如果用户权限返回空或者和拦截路径对应权限不同,验证不通过
ArrayList<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
GrantedAuthority au = new SimpleGrantedAuthority("eat");
list.add(au);
return list;
}
}

Security 的认证服务执行的时候,是通过传入一个 username 去寻找到这个 username 对应的 UserDetails 对象。

怎么去寻找,可以通过实现 UserDetailsService 接口的 loadUserByUsername() 方法实现。

这个类可以放在 Service 包下。

代码比较简单,注入一个 UserMapper ,就是一个 dao 嘛,我是用 MyBatis 实现的。

通过一个 username 去找到一个 Login 对象。

看上面就知道,Login 类就是实现 UserDetails 的类。

这里值得一提的是,下面这个这段查询,会用 userName 到数据库的 user 表中找到相应的一条记录,然后把对应的字段添加到 Login 类的属性中(通过 setter() 方法)。

所以 UserDetails 的实现类的属性不一定要完全包含 数据库表中的字段(但是属性名一定要与表中字段相同,这是 MyBatis 的知识。)。

我在百度的时候,那些比较易懂的教程都是专门在数据库建几张表然后,生成对应的 bean。

(方法有先建表,自动生成对应的 bean;先建 javabean 再自动生成表。)

其实可以根据自己的需要,通过联合查询找到自己需要的好几个表中的几个字段,设置一个 bean 即可。

比如 user 表有 id,name,role 表有 user.id 对应的 role ,我们就可以建一个有 id,name,role 属性的 javabean。

1
2
@Select("select * from user where name = #{userName}")
Login selectByUsername(String userName);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Login user = userMapper.selectByUsername(username);
if( user == null ){
throw new UsernameNotFoundException(String.format("User with username=%s was not found", username));
}
user.setRole("user");
return user;
}
}

这里得到了一个 user,我们就可以自己实现登录是否成功的判断啦。

authentication 里面有前端传来的数据。

然后比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class UserAuthenticationProvider implements AuthenticationProvider {
//需要这个吗?试试先
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
//从数据库读取的
Login userDetails = (Login) myUserDetailsService.loadUserByUsername(token.getName());
if (userDetails == null) {
throw new UsernameNotFoundException("找不到该用户");
}
if(!userDetails.getPassword().equals(token.getCredentials().toString()))
{
throw new BadCredentialsException("密码错误");
}
return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return false;
}
}

和数据库连接

Spring Security 和 SpringMvc 冲突了