SpringBoot安全认证Security

一、基本环境搭建

父pom依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version>
</parent>

1. 添加pom依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. 创建测试用Controller

@RestController
public class TestController {

    @GetMapping("getData")
    public String getData() {
        return "date";
    }

}

3. 创建SpringBoot启动类并run

@SpringBootApplication
public class SpringBootTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootTestApplication.class, args);
    }

}

4. 测试

访问http://127.0.0.1:8080/getData,由于我们开启了SpringSecurity且当前是未登录状态,页面会被302重定向到http://127.0.0.1:8080/login,页面如下:

用户名:user,密码可以在控制台输出中找到:

输入正确的用户名和密码后点击Login按钮即被重新302到http://127.0.0.1:8080/getData并显示查询数据:

这表示我们的接口已经被spring保护了。

那么肯定会有小伙伴吐槽了,这么复杂的密码,鬼才及得住,所以...

二、为Spring Security设定用户名和密码

为了解决复杂密码的问题,我们可以在application.yml中做如下设定:

spring:
  security:
    user:
      name: user
      password: 123

这样我们就可以通过用户名user密码123来访问http://127.0.0.1:8080/getData接口了。

然后肯定又有小伙伴吐槽了,整个系统就一个用户么?有哪个系统是只有一个用户的?所以...

三、为Spring Security设定多个用户

如果想要给Spring Security设定多个用户可用,则新建一个class:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig { @Bean public UserDetailsService userDetailsService() {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); InMemoryUserDetailsManager manager
= new InMemoryUserDetailsManager(); manager.createUser(User
.withUsername(
"admin")
.password("admin")
.passwordEncoder(encoder::encode)
.roles("")
.build()
); manager.createUser(User
.withUsername(
"guest")
.password("guest")
.passwordEncoder(encoder::encode)
.roles("")
.build()
);
return manager; } }
  1. 注意需要注解@EnableWebSecurity
  2. InMemoryUserDetailsManager:顾名思义,将用户名密码存储在内存中的用户管理器。我们通过这个管理器增加了两个用户,分别是:用户名admin密码admin,用户名guest密码guest。

做完如上更改后重启应用,再次访问http://127.0.0.1:8080/getData,输入admin/admin或guest/guest即可通过身份验证并正常使用接口了。

看到这肯定又有小伙伴要吐槽了:用户数据直接硬编码到代码里是什么鬼!我要把用户放在数据库!所以...

四、SpringSecurity+Mysql

想要使用数据库,那么我们可以

1. 增加如下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

2. 配置数据库连接

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.2.12:3306/test?characterEncoding=utf8
    username: root
    password: onceas

3. 创建测试用表结构及数据

drop table if exists test.user;
create table test.user (
  id int auto_increment primary key,
  username varchar(50),
  password varchar(50)
);

insert into test.user(id, username, password) values (1, 'admin', 'admin');
insert into test.user(id, username, password) values (2, 'guest', 'guest');

我们创建了用户信息表,并插入两个用户信息,用户名/密码依然是admin/admin、guest/guest

4. entity、dao、service

public class User {

    private int id;
    private String username;
    private String password;

    // get set ...
}
@Repository
public class LoginDao {

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public LoginDao(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public List<User> getUserByUsername(String username) {
        String sql = "select id, username, password from user where username = ?";
        return jdbcTemplate.query(sql, new String[]{username}, new BeanPropertyRowMapper<>(User.class));
    }
}
@Service
public class LoginService {

    private final LoginDao loginDao;

    @Autowired
    public LoginService(LoginDao loginDao) {
        this.loginDao = loginDao;
    }

    public List<User> getUserByUsername(String username) {
        return loginDao.getUserByUsername(username);
    }

}

5. 调整WebSecurityConfig

@Bean
public UserDetailsService userDetailsService() {
    return username -> {
        List<UserEntity> users = loginService.getUserByUsername(username);
        if (users == null || users.size() == 0) {
            throw new UsernameNotFoundException("用户名未找到");
        }
        String password = users.get(0).getPassword();
        PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        String passwordAfterEncoder = passwordEncoder.encode(password);
        return User.withUsername(username).password(passwordAfterEncoder).roles("").build();
    };
}

做完如上更改后重启应用,再次访问http://127.0.0.1:8080/getData,输入admin/admin或guest/guest即可通过身份验证并正常使用接口了。

关于UserDetailsService,有些东西要说明下:

PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String passwordAfterEncoder = passwordEncoder.encode(password);

上面这两句代码是在对用户密码进行加密。为什么要这样子呢?看到这肯定又有小伙伴会吐槽:数据库存储铭文密码是什么鬼!对,Spring也是尽量在帮助开发者避免这个事情。所以SpringSecurity在进行密码比对的时候需要开发者提供加密后的密码。我们上面的写法其实是不合理的,实际情况应该是数据库中存储密文密码,然后将数据库中的密码直接传给User.password()就可以了。

6. 关于SpringSecurity加密后的密文格式

我们可以通过打断点的方式或者增加

System.out.println(username + "---->>>" + passwordAfterEncoder);

来查看下,如果admin/admin被登录时候,passwordAfterEncoder的值是什么?输出结果:

admin---->>>{bcrypt}$2a$10$d4VkiIfP7MyNSipjLtQ0Keva4ST6U6Fnw77iiv39IGnGswptqWRG.
guest---->>>{bcrypt}$2a$10$8jRMbiGzFIS4GU3SWAm83eWgFO29EEb5QhXOEkPEaabw5Oiy/jxUC

可以看出加密后的密码可以分为两部分

  1. {}内描述了加密算法,这里为bcrypt算法。
  2. {}后面即为密文密码,这里是包含盐的。

所以SpringSecurity的工作原理就是:当用户输入用户名和密码点击Login以后,SpringSecurity先通过调用我们自定义的UserDetailsService获取到加密后密码,然后根据{}里的内容获知加密算法,再将用户输入的密码按照该算法进行加密,最后再与{}后的密文密码比对即可获知用户凭据是否有效。

通过查看PasswordEncoderFactories的源码,我们可以知道SpringEncoder工具可以提供哪些加密算法:

public static PasswordEncoder createDelegatingPasswordEncoder() {
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap", new LdapShaPasswordEncoder());
        encoders.put("MD4", new Md4PasswordEncoder());
        encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("sha256", new StandardPasswordEncoder());
        return new DelegatingPasswordEncoder(encodingId, encoders);
    }

其中LdapShaPasswordEncoder、Md4PasswordEncoder、MessageDigestPasswordEncoder、NoOpPasswordEncoder、StandardPasswordEncoder已经不建议使用了。SpringSecurity认为:

Digest based password encoding is not considered secure.  //基于摘要的密码编码被认为是不安全的

五 、权限控制

以上内容我们只解决了用户登录问题,但是实际开发中仅仅完成用户登录是不够的,我们还需要用户授权及授权验证。由于我们已经将用户信息存储到数据库里了,那么姑且我们也将权限信息存储在数据库吧。

1. 准备数据库表及测试数据

drop table if exists test.role;
create table test.role (
  id int auto_increment primary key,
  role varchar(50)
);

drop table if exists test.permission;
create table test.permission (
  id int auto_increment primary key,
  permission varchar(50)
);

drop table if exists test.user_r_role;
create table test.user_r_role (
  userid int,
  roleid int
);

drop table if exists test.role_r_permission;
create table test.role_r_permission (
  roleid int,
  permissionid int
);

drop table if exists test.user_r_permission;
create table test.user_r_permission (
  userid int,
  permissionid int
);

insert into test.role(id, role) values (1, 'adminRole');
insert into test.role(id, role) values (2, 'guestRole');

insert into test.permission(id, permission) values (1, 'permission1');
insert into test.permission(id, permission) values (2, 'permission2');
insert into test.permission(id, permission) values (3, 'permission3');
insert into test.permission(id, permission) values (4, 'permission4');

insert into test.user_r_role(userid, roleid) values (1, 1);
insert into test.user_r_role(userid, roleid) values (2, 2);

insert into test.role_r_permission(roleid, permissionid) values (1, 1);
insert into test.role_r_permission(roleid, permissionid) values (1, 2);

insert into test.user_r_permission(userid, permissionid) values (1, 3);
insert into test.user_r_permission(userid, permissionid) values (1, 4);
insert into test.user_r_permission(userid, permissionid) values (2, 3);
insert into test.user_r_permission(userid, permissionid) values (2, 4);
View Code
  1. role:角色信息表,permission权限信息表,user_r_role用户所属角色表,role_r_permission角色拥有权限表,user_r_permission用户拥有权限表。
  2. 由于用户有所属角色且角色是有权限的,用户同时又单独拥有权限,所以用户最终拥有的权限取并集。
  3. 用户admin最终拥有角色adminRole以及权限:permission1、permission2、permission3、permission4
  4. 用户guest最终拥有角色guestRole以及权限:permission3、permission4

2. Dao、Service

public List<String> getPermissionsByUsername(String username) {
    String sql =
            "select d.permission\n" +
            "from user a\n" +
            "       join user_r_role b on a.id = b.userid\n" +
            "       join role_r_permission c on b.roleid = c.roleid\n" +
            "       join permission d on c.permissionid = d.id\n" +
            "where a.username = ?\n" +
            "union\n" +
            "select c.permission\n" +
            "from user a\n" +
            "       join user_r_permission b on a.id = b.userid\n" +
            "       join permission c on b.permissionid = c.id\n" +
            "where a.username = ?";
    return jdbcTemplate.queryForList(sql, new String[]{username, username}, String.class);
}

public List<String> getRoleByUsername(String username) {
    String sql =
            "select c.role\n" +
            "from user a\n" +
            "       join user_r_role b on a.id = b.userid\n" +
            "       join role c on b.roleid = c.id\n" +
            "where a.username = ?";
    return jdbcTemplate.queryForList(sql, new String[]{username}, String.class);
}
dao增加方法:根据用户名查角色以及根据用户名查权限
public List<String> getPermissionsByUsername(String username) {
    return loginDao.getPermissionsByUsername(username);
}

public List<String> getRoleByUsername(String username) {
    return loginDao.getRoleByUsername(username);
}
service增加方法:根据用户名查角色以及根据用户名查权限

3. WebSecurityConfig

(1)调整public UserDetailsService userDetailsService()方法,在构建用户信息的时候把用户所属角色和用户所拥有的权限也填充上(最后return的时候)。

@Bean
public UserDetailsService userDetailsService() {
    return username -> {
        List<UserEntity> users = loginService.getUserByUsername(username);
        if (users == null || users.size() == 0) {
            throw new UsernameNotFoundException("用户名未找到");
        }
        String password = users.get(0).getPassword();
        PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        String passwordAfterEncoder = passwordEncoder.encode(password);
        System.out.println(username + "/" + passwordAfterEncoder);

        List<String> roles = loginService.getRoleByUsername(username);
        List<String> permissions = loginService.getPermissionsByUsername(username);

        String[] roleArr = new String[roles.size()];
        String[] permissionArr = new String[permissions.size()];

        return User.withUsername(username).password(passwordAfterEncoder).
                roles(roles.toArray(roleArr)).authorities(permissions.toArray(permissionArr)).
                build();
    };
}

这里面有个坑,就是红色代码部分。具体可查看org.springframework.security.core.userdetails.User.UserBuilder。roles()方法和authorities()方法实际上都是在针对UserBuilder的authorities属性进行set操作,执行roles("roleName")和执行authorities("ROLE_roleName")是等价的所以上例代码中roles(roles.toArray(roleArr))起不到任何作用,直接被后面的authorities(permissions.toArray(permissionArr))覆盖掉了。

所以正确的写法可参考:

@Bean
public UserDetailsService userDetailsService() {
    return username -> {
        List<UserEntity> users = loginService.getUserByUsername(username);
        if (users == null || users.size() == 0) {
            throw new UsernameNotFoundException("用户名未找到");
        }
        String password = users.get(0).getPassword();
        PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        String passwordAfterEncoder = passwordEncoder.encode(password);
        System.out.println(username + "/" + passwordAfterEncoder);

        List<String> roles = loginService.getRoleByUsername(username);
        List<String> permissions = loginService.getPermissionsByUsername(username);

        String[] permissionArr = new String[roles.size() + permissions.size()];
        int permissionArrIndex = 0;
        for (String role : roles) {
            permissionArr[permissionArrIndex] = "ROLE_" + role;
            permissionArrIndex++;
        }
        for (String permission : permissions) {
            permissionArr[permissionArrIndex] = permission;
            permissionArrIndex++;
        }
        return User.withUsername(username).password(passwordAfterEncoder).authorities(permissionArr).build();
    };
}

(2)增加新的bean,为我们需要的保护的接口设定需要权限验证:

@Bean
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
    return new WebSecurityConfigurerAdapter() {
        @Override
        public void configure(HttpSecurity httpSecurity) throws Exception {
            httpSecurity.
                    authorizeRequests().antMatchers("/guest/**").permitAll().
                    and().authorizeRequests().antMatchers("/admin/**").hasRole("admin").
                    and().authorizeRequests().antMatchers("/authenticated/**").authenticated().
                    and().authorizeRequests().antMatchers("/permission1/**").hasAuthority("permission1").
                    and().authorizeRequests().antMatchers("/permission2/**").hasAuthority("permission2").
                    and().authorizeRequests().antMatchers("/permission3/**").hasAuthority("permission3").
                    and().authorizeRequests().antMatchers("/permission4/**").hasAuthority("permission4").
                    and().formLogin().
                    and().authorizeRequests().anyRequest().permitAll();
        }
    };
}
  1. /guest/**的接口会被允许所有人访问,包括未登录的人。
  2. /admin/**的接口只能被拥有admin角色的用户访问。
  3. /authenticated/**的接口可以被所有已经登录的用户访问。
  4. /permission1/**的接口可以被拥有permission1权限的用户访问。/permission2/**、/permission3/**、/permission4/**同理

4. TestController

最后我们调整下TestContrller,增加几个接口以便测试:

@RestController
public class TestController {

    @GetMapping("getData")
    public String getData() {
        return "date";
    }

    @GetMapping("authenticated/getData")
    public String getAuthenticatedData() {
        return "authenticatedData";
    }

    @GetMapping("admin/getData")
    public String getAdminData() {
        return "adminData";
    }

    @GetMapping("guest/getData")
    public String getGuestData() {
        return "guestData";
    }

    @GetMapping("permission1/getData")
    public String getPermission1Data() {
        return "permission1Data";
    }

    @GetMapping("permission2/getData")
    public String getPermission2Data() {
        return "permission2Data";
    }

    @GetMapping("permission3/getData")
    public String getPermission3Data() {
        return "permission3Data";
    }

    @GetMapping("permission4/getData")
    public String getPermission4Data() {
        return "permission4Data";
    }

}
TestController

5. 测试

  • 访问/guest/getData无需登录即可访问成功。
  • 访问/authenticated/getData,会弹出用户登录页面。登录任何一个用户都可访问成功。
  • 访问/admin/getData,会弹出用户登录页面。登录admin用户访问成功,登录guest用户会发生错误,403未授权。
  • 其他的就不再赘述了。

六、自定义登录页面

是不是觉得SpringScurity的登录页面丑爆了?是不是想老子还能做一个更丑的登录页面你信不信?接下来我们来弄一个更丑的登录页面。

1. 增加pom依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

2. 编写自己的登录页面

thymeleaf默认的页面放置位置为:classpath:templates/ 目录下,所以在编写代码的时候我们可以将页面放在resources/templates目录下,名称为:login.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>一个更丑的登录页面</title>
</head>
<body>
    <form method="post" action="/login">
        用户名:<input name="username" placeholder="请输入用户名" type="text">
        密码:<input name="password" placeholder="请输入密码" type="password">
        <input value="登录" type="submit">
    </form>
</body>
</html>

3. 将SpringSecurity指向自定义的登录页面

(1)调整WebSecurityConfig注入的WebSecurityConfigurerAdapter,在and().formLogin()后面增加loginPage("/login")以指定登录页面的uri地址,同时关闭csrf安全保护。

@Bean
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
    return new WebSecurityConfigurerAdapter() {
        @Override
        public void configure(HttpSecurity httpSecurity) throws Exception {
            httpSecurity.
                    authorizeRequests().antMatchers("/guest/**").permitAll().
                    ...省略部分代码...
                    and().formLogin().loginPage("/login").
                    and().authorizeRequests().anyRequest().permitAll().
                    and().csrf().disable();
        }
    };
}

(2)TestController增加login方法(注意我们之前在TestController类上注解了@RestController,这里要记得改成@Controller,否则访问/login的时候会直接返回字符串而不是返回html页面。另外除了下面新增的/login方法其他方法要增加注解@ResponseBody)

@GetMapping("login")
public String login() {
    return "login";
}

4. 测试及其他

测试过程就略吧。还有一些要嘱咐的东西给小白们:

  • 我们通过loginPage("/login")来告知SpringSecurity自定义登录页面的uri路径,同时这个设定也告知了用户点击登录按钮的时候form表单post的uri路径。即:如果SpringSecurity判定需要用户登录,会将302到/login (get请求),用户输入用户名和密码点击登录按钮后,也需要我们自定义页面post到/login才能让SpringSecurity完成用户认证过程。
  • 关于html中输入用户名的input的name属性值本例为username、输入密码的input的name属性值本例为password,这是因为SpringSecurity在接收用户登录请求时候默认的参数名就是username和password、如果想更改这两个参数名,可以这样设定:and().formLogin().loginPage("/login").usernameParameter("username").passwordParameter("password")
  • 测试过程中我们可以试着输错用户名和密码点击登录,会发现页面又重新跳转到 http://127.0.0.1:8080/login?error ,只不过后面增加了参数error且没有参数值。所以需要我们再login.html中处理相应的逻辑。当然你也可以指定用户认证失败时候的跳转地址,可以这样设定:and().formLogin().loginPage("/login").failureForwardUrl("/login/error")
  • 测试过程中,如果我们直接访问http://127.0.0.1:8080/login,输入正确的用户名和密码后跳转到http://127.0.0.1:8080即网站根目录。如果你想指定用户登录成功后的默认跳转地址,可以这样设定:and().formLogin().loginPage("/login").successForwardUrl("/login/success")

七、登出

登出呢?有登录了,怎么能没有登出呢?其实SpringSecurity已经早早的为我们默认了一个登出功能,你访问:http://127.0.0.1:8080/logout 试试看?

如果想做我们自己的个性化登出,可以继续调整WebSecurityConfig注入的WebSecurityConfigurerAdapter

@Bean
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
    return new WebSecurityConfigurerAdapter() {
        @Override
        public void configure(HttpSecurity httpSecurity) throws Exception {
            httpSecurity.
                    authorizeRequests().antMatchers("/guest/**").permitAll().
                    and().authorizeRequests().antMatchers("/admin/**").hasRole("admin").
                    and().authorizeRequests().antMatchers("/authenticated/**").authenticated().
                    and().authorizeRequests().antMatchers("/permission1/**").hasAuthority("permission1").
                    and().authorizeRequests().antMatchers("/permission2/**").hasAuthority("permission2").
                    and().authorizeRequests().antMatchers("/permission3/**").hasAuthority("permission3").
                    and().authorizeRequests().antMatchers("/permission4/**").hasAuthority("permission4").
                    and().formLogin().loginPage("/login").
                    and().logout().logoutUrl("/logout").logoutSuccessUrl("/logoutSuccess").
invalidateHttpSession(true).deleteCookies("cookiename").
addLogoutHandler(new MyLogoutHandle()).logoutSuccessHandler(new
MyLogoutSuccessHandle()). and().authorizeRequests().anyRequest().permitAll(). and().csrf().disable(); } }; }
  • MyLogoutHandle实现了LogoutHandler接口:
    public class MyLogoutHandle implements LogoutHandler {
    
        @Override
        public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
            System.out.println("==================>>>> LogoutHandler Begin");
            System.out.println(authentication.getPrincipal());
            System.out.println("==================>>>> LogoutHandler End");
        }
    }
    MyLogoutHandle
  • MyLogoutSuccessHandle实现了LogoutSuccessHandler接口:
    public class MyLogoutSuccessHandle implements LogoutSuccessHandler {
    
        @Override
        public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
            System.out.println("==================>>>> LogoutSuccessHandler Begin");
            System.out.println(authentication.getPrincipal());
            System.out.println("==================>>>> LogoutSuccessHandler End");
        }
    }
    MyLogoutSuccessHandle
  • logoutUrl():告诉SpringSecurity用户登出的接口uri地址是什么
  • logoutSuccessUrl():告诉SpringSecurity完成用户登出后要跳转到哪个地址。如果设定了LogoutSuccessHandler则logoutSuccessUrl设定无效
  • invalidateHttpSession:执行登出的同时是否清空session
  • deleteCookies:执行登出的同时删除那些cookie
  • addLogoutHandler:执行登出的同时执行那些代码

八、SpringSecurity在Restfull中的变通使用

当前环境前后盾分离已经是大趋势了吧,除非那些很小很小的项目。所以SpringBoot项目更多的时候为前端提供接口,而并不提供前端页面路由的功能。所以,当SpringSecurity在Restfull开发中还需要变通一下:

  1. 首先我们通过and().formLogin().loginPage("/login")设定的跳转到登录页面的GET请求不再指向html,而是直接返回json数据告知前端需要用户登录。
  2. 用户执行登录的时候,前端执行post请求到/login进行用户身份校验。
  3. 然后我们通过and().formLogin().failureForwardUrl("/login/error")和and().formLogin().successForwardUrl("/login/error")设定的登录成功和失败跳转来地址来返回json数据给前端告知其用户认证结果。
  4. 最后我们通过and().logout().logoutSuccessHandler(new MyLogoutSuccessHandle())来返回json数据给前端告知用户已经完成登出。

九、SpringSecurity+SpringSession+Redis

接下来还有一个问题要处理。在上面的案例中,session都是存储在servlet容器中的,如果我们需要多点部署负载均衡的话,就会出现问题。比如:我们部署了两个服务并做了负载均衡,用户登录时调用其中一台服务进行身份认证通过并将用户登录信息存储在了这台服务器的session里,接下来用户访问其他接口,由于负载均衡的存在用户请求被分配到了另一个服务上,该服务检测用户session不存在啊,于是就拒绝访问。

在SpringBoot环境下解决这个问题也很简答,很容易就想到SpringSession。所以我们尝试用SpringSession+Redis解决此问题

1. 增加pom依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

2. 修改application.yml

spring:
  redis:
    host: 192.168.2.12
    port: 6379
    password: 123456

  session:
    store-type: redis

3. 修改主启动类,增加@EnableRedisHttpSession注解,开启SpringSession

十、通过注解的方式实现权限控制

首先要在主启动类上增加@EnableGlobalMethodSecurity注解,具体参数如下:

1. @EnableGlobalMethodSecurity(securedEnabled=true)

支持@Secured注解,例如

@Secured("ROLE_adminRole")

2. @EnableGlobalMethodSecurity(jsr250Enabled=true)

支持@RolesAllowed、@DenyAll、@PermitAll 注解,例如:

@RolesAllowed("ROLE_guestRole")

3. @EnableGlobalMethodSecurity(prePostEnabled=true)

支持@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter注解,它们使用SpEL能够在方法调用上实现更有意思的安全性约束

  • @PreAuthorize :在方法调用之前,基于表达式的计算结果来限制对方法的访问,只有表达式计算结果为true才允许执行方法
  • @PostAuthorize 在方法调用之后,允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
  • @PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
  • @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值

由于这里涉及到SpEL表达式,所以本文就不详细说了。

十一、在Controller中获取当前登录用户

public String getAuthenticatedData(HttpSession session) {
        //SecurityContext securityContext = SecurityContextHolder.getContext();
        SecurityContext securityContext = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT");
        // 以上获取securityContext的两种方法二选一
        WebAuthenticationDetails userDetailsService = (WebAuthenticationDetails) securityContext.getAuthentication().getDetails();
        UserDetails userDetails = (UserDetails) securityContext.getAuthentication().getPrincipal();

        System.out.println("===userDetailsService.getRemoteAddress()===>>" + userDetailsService.getRemoteAddress());
        System.out.println("===userDetailsService.getSessionId()===>>" + userDetailsService.getSessionId());
        System.out.println("===userDetails.getRemoteAddress()===>>" + userDetails.getUsername());
        System.out.println("===userDetails.getPassword()===>>" + userDetails.getPassword());
        System.out.println("===userDetails.getAuthorities()===>>" + userDetails.getAuthorities());
        return "authenticatedData";
    }

十二、总结

SpringSecurity的使用基本就上面这些。就业务逻辑来说,SpringSecurity中所谓的role概念严格意义并不能称之为“角色”。理由是:如果我们的权限控制比较简单,整个系统中的角色以及角色所拥有的权限是固定的,那么我们可以将SpringSecurity的role概念拿来即用。但是如果我们的权限控制是可配置,用户和角色是多对多关系、角色和权限也是多对多关系,那么我们只能讲SpringSecurity的role当做“权限”来使用。

 

posted @ 2018-11-05 16:17  柚子苹果果  阅读(6236)  评论(0编辑  收藏  举报