Spring Security 快速应用

注意
  • 本文涉及的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,出现如下界面:
Spring Security 默认登陆界面
明显的,直接访问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
如下示范:
自定义Spring Security访问
自定义Spring Security访问
到此基本实现自定义的登陆过程:

  • 后端仅提供一个登陆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();


    }
}
参考
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值