注意
- 本文涉及的Spring Security版本为
5.1.4
- 考虑的是前后端分离的工程
基本的需求是什么:
- 辅助用户的身份验证过程
- URL 访问保护
最小工程
IntellJ IDEA
开发工具中可以直接选择生成Springboot工程,只需要 Web+Security ,搭个工程分分钟。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
最简单的 URL
工程中随意创建个类就行
@RestController
public class AuthenticationExample {
@GetMapping("/helloworld")
public String helloworld(){
return "Hello World";
}
}
OK, 直接运行工程,注意控制台
会打印一串密码
Using generated security password: 11b929c5-0f90-435d-bf34-15354f0b9242
然后在浏览器中访问url http://localhost:8080/helloworld
,出现如下界面:
明显的,直接访问url路径是得不到想要的Hello World
,Spring Security拦截了url并引导进行登陆验证,起到了限制访问url的作用。那么登陆吧,默认的用户名user
,默认的密码就是刚才工程控制台
打印的,输入后就可以看到正确的url返回结果:
使用自己的用户
实际工作中没人会用这默认登陆吧,那么Spring Security 怎么识别我们自己定义的用户呢? 所以还要做的工作如下:
(1)编写一个类实现接口UserDetailsService
这样框架在运行时会调用我们的实现类,查找我们自己定义的用户信息进行验证。loadUserByUsername( String s)
方法就是入口,s
为登陆传入的用户名,从而你可以拿着用户名去数据库里找密码等用户信息,抽取出密码以及权限列表构建为UserDetails
对象返回给框架。
@Component
public class UserDetailsServiceImplement implements UserDetailsService {
//自定义用户的 用户名/密码/角色列表,这里仅为示范,正常来说应该是从数据库等查找出
static final String user = "666";
static final String password = new BCryptPasswordEncoder().encode("123456");
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
//没数据库,将就设置一个示范的角色 ROLE_USER
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if( user.equals(s)){
return new org.springframework.security.core.userdetails.User(s,password,AUTHORITIES);
}else {
throw new UsernameNotFoundException("Not Found");
}
}
}
(2)编写一个配置类,定义加密方式。
Spring Security 5.0
之后要求提供密码的加密方式,推荐使用BCryptPasswordEncoder
。
你不会把密码之类的明文放在数据库吧?
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
//假如输入密码为 123456 那么加密后为 $2a$10$GPl.ntcYa9jGGEHjMOM3BuQ4YoHyI1HCDSZo1uu6RC178XTxgxhRW
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
(3)OK,运行工程,访问url http://localhost:8080/helloworld
,(参考代码)在登陆页面输入自定义用户名666
,密码123456
,登入成功!
使用自己的登陆入口
以上依然在默认页面中进行登陆过程,还不满足工作需要。对于一个前后端分离的工程,后端一般木有页面吧。前端直接携带用户登陆信息请求后端。那么,我们改造一下之前的工程:
(1)改造之前的配置类 WebSecurityConfig
,修改为如下:
- 继承 WebSecurityConfigurerAdapter
- 覆写 authenticationManagerBean() ,方便使用 AuthenticationManager 自定义登陆
- 覆写 configure(HttpSecurity http) ,开放一个自定义登陆入口 url
/user/login
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/login").permitAll()
.anyRequest().authenticated()
}
}
(2)改造之前的 url 访问类 AuthenticationExample
- 新添加访问url 处理方法
userLogin()
,对应之前配置类中开放的的url/user/login
。实际工程中建议用Post方法,安全性更高。 UsernamePasswordAuthenticationToken
利用传入的用户名与密码构造框架可以识别的对象AuthenticationManager
是框架提供的验证管理类,也会调用我们之前实现的用户类UserDetailsServiceImplement
进行验证过程SecurityContextHolder
负责将验证后的用户登陆信息保存起来,登陆过的用户就不用再次登陆啦,开始接受框架的管理。如果AuthenticationManager
验证失败了会直接抛出异常进入失败处理过程,并不会留下用户登陆信息哟。
@RestController
public class AuthenticationExample {
private final AuthenticationManager authenticationManager;
public AuthenticationExample(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@GetMapping("/helloworld")
public String helloworld(){
return "Hello World";
}
@GetMapping("/user/login")
public String userLogin(@RequestParam("username") String username , @RequestParam("password") String password ){
boolean loginResult = true;
try {
//验证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);
Authentication authentication = this.authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}catch (AuthenticationException e){
loginResult = false;
}
return loginResult? "Success" : "Fail";
}
}
(3)OK,运行工程,然后浏览器模拟一个前端的带参数请求:
http://localhost:8080/user/login?username=666&password=123456
如下示范:
到此基本实现自定义的登陆过程:
- 后端仅提供一个登陆URL
/user/login
,前端提交用户登陆信息并获取验证结果。 - 前端拿到结果可以控制前端路由进行页面跳转
使用角色控制URL的访问
经过之前的步骤,仅仅是做到了登陆之前无法访问被保护的url,登陆之后可以访问所有url。然而,应该是不同角色登陆后享受不同的访问权限呀!角色(例如之前代码中配置了 ROLE_USER
)该如何用起来呢?
(1)改造之前的配置类WebSecurityConfig
启用 prePostEnabled
,这样就可以以注解的方式设置权限
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
(2)改造之前的URL类AuthenticationExample
在 helloword()
方法前加上注解 @PreAuthorize
@PreAuthorize("hasRole('ROLE_USER')")
@GetMapping("/helloworld")
public String helloworld(){
return "Hello World";
}
(3)OK,运行工程,让我们再次浏览器模拟登陆
http://localhost:8080/user/login?username=666&password=123456
验证为success后,浏览器访问http://localhost:8080/helloworld
,可以正常访问
(4)停止运行,改造我们的用户验证实现类UserDetailsServiceImplement
将角色 ROLE_USER
改为ROLE_ADMIN
//没数据库,将就设置一个示范的角色 ROLE_USER
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
重复步骤3,发现同样的操作却结果访问不了http://localhost:8080/helloworld
,这就是注解利用角色进行了拦截,只有对应的角色才能访问。
(5)角色组合。 ROLE_USER
是普通角色,如果ROLE_ADMIN
是高级角色那么理应也具备普通角色的访问权限才对,这里可以添加一个helloworld02()
方法,使用组合角色表达式进行区分:
@PreAuthorize("hasRole('ROLE_USER') AND hasRole('ROLE_ADMIN') " )
@GetMapping("/helloworld")
public String helloworld(){
return "Hello World";
}
@PreAuthorize("hasRole('ROLE_ADMIN')" )
@GetMapping("/helloworld02")
public String helloworld02(){
return "Hello World 02";
}
重复步骤(3)(4),可以发现ROLE_USER
可以访问/helloworld
,不可访问/helloworld2
,而ROLE_ADMIN
具有两者的访问权限。至此我们通过角色进行了权限区分。
登陆用户的重复登陆限制与有效期限
登陆用户默认有效期为30分钟(内部容器默认的),可以在application.properties
设置有效期限:
//设置有效期为60秒 示范
server.servlet.session.timeout=60
过期后用户将不可用,用户刷新页面将会被拒绝,前端收到403错误码可以自行控制前端路由跳转进行重新登陆。过期后访问信息如下:
有时候需要限制用户共享账号,导致重复登陆,Spring Security提供了一个很简单的配置方法,如下:
(1)改造之前的配置类 WebSecurityConfig
将其中configure(HttpSecurity http)
方修改如下
maximumSessions(1)
控制同时会话数的上线,这里限定只能登陆一个.maxSessionsPreventsLogin(false);
false为后登陆会让之前的会话失效;true为后续登陆必须等待已登陆会话失效才能创建。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/login").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(false);
}
最后贴上完整代码(就三个类)
AuthenticationExample
@RestController
public class AuthenticationExample {
private final AuthenticationManager authenticationManager;
public AuthenticationExample(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@PreAuthorize("hasRole('ROLE_USER') AND hasRole('ROLE_ADMIN') " )
@GetMapping("/helloworld")
public String helloworld(){
return "Hello World";
}
@PreAuthorize("hasRole('ROLE_ADMIN')" )
@GetMapping("/helloworld02")
public String helloworld02(){
return "Hello World 02";
}
@GetMapping("/user/login")
public String userLogin(@RequestParam("username") String username , @RequestParam("password") String password ){
boolean loginResult = true;
try {
//验证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);
Authentication authentication = this.authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}catch (AuthenticationException e){
loginResult = false;
}
return loginResult? "Success" : "Fail";
}
@GetMapping("/user/session/invalid")
public ResponseEntity<String> userSessionInvaild(){
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("会话过期了,请重新登陆");
}
}
UserDetailsServiceImplement
@Component
public class UserDetailsServiceImplement implements UserDetailsService {
//自定义用户的 用户名/密码/角色列表,这里仅为示范,正常来说应该是从数据库等查找出
static final String user = "666";
static final String password = new BCryptPasswordEncoder().encode("123456");
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
//没数据库,将就设置一个示范的角色 ROLE_USER
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if( user.equals(s)){
return new org.springframework.security.core.userdetails.User(s,password,AUTHORITIES);
}else {
throw new UsernameNotFoundException(" Not Found");
}
}
}
WebSecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//加入密码为 123456 加密后为 $2a$10$GPl.ntcYa9jGGEHjMOM3BuQ4YoHyI1HCDSZo1uu6RC178XTxgxhRW
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/login").permitAll()
.anyRequest().authenticated();
}
}