Spring Boot Security 数据库方式入门案例

案例中有使用mongodb数据库,具体的spring boot整合mongodb的案例参考地址http://www.leftso.com/blog/135.html

Spring Security有一些使用复杂的意见。那当然,当你看的时候,它的范围很复杂,因为它的范围涵盖了大量的用例。事实上,真正的spring的精神,你不必一次使用你所拥有的用例的一切功能。事实上,当你开始使用Spring Boot进行樱桃挑选并将其恢复时,它似乎并不复杂。

我们从使用情况开始,我在想一些比较常见的东西,几乎每个项目都出现一些基本的访问限制。所以,这样一个应用程序的要求可以是:

  • 该应用将有用户,每个用户角色为管理员或用户
  • 他们通过电子邮件和密码登录
  • 非管理员用户可以查看他们的信息,但不能窥视其他用户
  • 管理员用户可以列出并查看所有用户,并创建新的用户
  • 定制表单登录
  • “记住我”验证懒惰
  • 注销的可能性
  • 主页将提供给所有人,不经过验证

这样的应用程序源代码在GitHub上,供您查看。建议您将源代码打开,以下将是某些关键点的评论。啊,和安全相关的东西接近尾声,所以只要知道基础知识就可以向下滚动。

依赖关系

此外,标准的Spring Boot依赖关系,最重要的依赖关系是Spring Security,Spring Data JPA的启动器模块,因为我们需要某处存储用户,而嵌入式内存中的HSQLDB作为存储引擎。

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

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

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

<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
</dependency>

在现实生活中,您可能会更有兴趣连接到某种外部数据库引擎,例如本文中关于使用BoneCP的数据库连接池的描述。我也使用Freemarker作为模板引擎,但是如果这不是你的事情,那么对于JSP来说,重写它也应该很简单,就像在本文中关于Spring MVC应用程序一样

域模型

所以我们会有这样的User实体:

@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false, updatable = false)
    private Long id;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "password_hash", nullable = false)
    private String passwordHash;

    @Column(name = "role", nullable = false)
    @Enumerated(EnumType.STRING)
    private Role role;

    // getters, setters

}

如您所见,只有密码的哈希将存储在数据库中,这通常是一个好主意。该email领域还有一个独特的约束,但它不是主要的关键。由于id某个原因,电子邮件地址是您不想在访问日志中显示的相当敏感的信息,因此用户被标识,所以我们将尽可能地使用id。

Role 是一个简单的枚举:

public enum Role {
    USER, ADMIN
}

除此之外,创建新用户的表单将是很好的:

public class UserCreateForm {

    @NotEmpty
    private String email = "";

    @NotEmpty
    private String password = "";

    @NotEmpty
    private String passwordRepeated = "";

    @NotNull
    private Role role = Role.USER;

}

这将用作Web层和服务层之间的数据传输对象(DTO)。它由Hibernate Validator验证约束注释,并设置一些合理的默认值。请注意,它与User对象略有不同,因此我希望将User实体“泄漏” 到Web层中,我真的不能。

这就是我们现在所需要的。

服务层

在服务层,业务逻辑应该是什么,我们需要一些东西来检索User他的id,电子邮件,列出所有的用户并创建一个新的用户。

所以接口将是:

public interface UserService {

    Optional<User> getUserById(long id);

    Optional<User> getUserByEmail(String email);

    Collection<User> getAllUsers();

    User create(UserCreateForm form);

}

服务的实现:

@Service
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public Optional<User> getUserById(long id) {
        return Optional.ofNullable(userRepository.findOne(id));
    }

    @Override
    public Optional<User> getUserByEmail(String email) {
        return userRepository.findOneByEmail(email);
    }

    @Override
    public Collection<User> getAllUsers() {
        return userRepository.findAll(new Sort("email"));
    }

    @Override
    public User create(UserCreateForm form) {
        User user = new User();
        user.setEmail(form.getEmail());
        user.setPasswordHash(new BCryptPasswordEncoder().encode(form.getPassword()));
        user.setRole(form.getRole());
        return userRepository.save(user);
    }

}

这里不值得一个评论 - 服务代理到UserRepository大部分时间。值得注意的是,在该create()方法中,该表单用于构建一个新User对象。哈希是从使用的密码BCryptPasswordEncoder生成的,这应该比臭名昭着的MD5产生更好的哈希。

UserRepository定义如下:

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findOneByEmail(String email);
}

这里只添加一个非默认方法findOneByEmail。请注意,我希望它返回User包装在JDK8中Optional,这是Spring的一个新功能,并且使处理null值更容易。

Web层

这是控制者及其观点。为了满足应用的要求,我们至少需要一对夫妇。

主页

我们将处理/网站的根HomeController,其中只返回一个home视图:

@Controller
public class HomeController {

    @RequestMapping("/")
    public String getHomePage() {
        return "home";
    }

}

用户列表

同样,用户的列表将被映射到/users并由其处理UsersController。它UserService注入,要求它返回CollectionUser对象,将它们放入了users模型属性,然后调用users视图名称:

@Controller
public class UsersController {

    private final UserService userService;

    @Autowired
    public UsersController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping("/users")
    public ModelAndView getUsersPage() {
        return new ModelAndView("users", "users", userService.getAllUsers());
    }

}

查看和创建用户

接下来,我们需要一个控制器来处理查看和创建一个新的用户,我称之为它UserController,它更复杂:

@Controller
public class UserController {

    private final UserService userService;
    private final UserCreateFormValidator userCreateFormValidator;

    @Autowired
    public UserController(UserService userService, UserCreateFormValidator userCreateFormValidator) {
        this.userService = userService;
        this.userCreateFormValidator = userCreateFormValidator;
    }

    @InitBinder("form")
    public void initBinder(WebDataBinder binder) {
        binder.addValidators(userCreateFormValidator);
    }

    @RequestMapping("/user/{id}")
    public ModelAndView getUserPage(@PathVariable Long id) {
        return new ModelAndView("user", "user", userService.getUserById(id)
                .orElseThrow(() -> new NoSuchElementException(String.format("User=%s not found", id))));
    }

    @RequestMapping(value = "/user/create", method = RequestMethod.GET)
    public ModelAndView getUserCreatePage() {
        return new ModelAndView("user_create", "form", new UserCreateForm());
    }

    @RequestMapping(value = "/user/create", method = RequestMethod.POST)
    public String handleUserCreateForm(@Valid @ModelAttribute("form") UserCreateForm form, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "user_create";
        }
        try {
            userService.create(form);
        } catch (DataIntegrityViolationException e) {
            bindingResult.reject("email.exists", "Email already exists");
            return "user_create";
        }
        return "redirect:/users";
    }

}

视图被映射到/user/{id}URL,由getUserPage()方法处理。它要求UserService一个用户id,它是从URL中提取的,并作为参数传递给此方法。你记得,UserService.getUserById()返回一个User包装的实例Optional。因此,.orElseThrow()被要求Optional获得一个User 实例,或者在它的时候抛出异常null

创建一个新User的映射到/user/create并由两种方法处理:getUserCreatePage()handleUserCreateForm()。第一个只返回一个user_create具有空格式的视图作为form模型的属性。另一个响应POST请求,并UserCreateForm作为参数进行验证。如果表单中有错误,则由BindingResult该视图返回。如果表单确定,则将其传递给UserService.create()方法。

还有一个额外的检查DataIntegrityViolationException。如果发生这种情况,则认为是因为尝试User使用数据库中已经存在的电子邮件地址来创建,因此再次呈现该表单。

在现实生活中,知道哪个约束被严重违反是非常困难的(或者与ORM无关),所以当做这些假设时,至少应该记录异常情况以便进一步检查。应该注意防止这种异常发生在第一位,如对于重复的电子邮件的形式的正确验证。

否则,如果一切正常,则重定向到/usersURL。

定制验证 UserCreateForm

@InitBinder在注解的方法UserController添加UserCreateFormValidatorform参数,告诉它应该由它来验证。这样做的原因在于,UserCreateForm需要对两种可能的情况进行整体验证:

  • 检查密码和重复密码是否匹配
  • 检查电子邮件是否存在于数据库中

这样做,UserCreateFormValidator实现如下:

@Component
public class UserCreateFormValidator implements Validator {

    private final UserService userService;

    @Autowired
    public UserCreateFormValidator(UserService userService) {
        this.userService = userService;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.equals(UserCreateForm.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        UserCreateForm form = (UserCreateForm) target;
        validatePasswords(errors, form);
        validateEmail(errors, form);
    }

    private void validatePasswords(Errors errors, UserCreateForm form) {
        if (!form.getPassword().equals(form.getPasswordRepeated())) {
            errors.reject("password.no_match", "Passwords do not match");
        }
    }

    private void validateEmail(Errors errors, UserCreateForm form) {
        if (userService.getUserByEmail(form.getEmail()).isPresent()) {
            errors.reject("email.exists", "User with this email already exists");
        }
    }
}

在登录

它将映射到/loginURL并处理LoginController

@Controller
public class LoginController {

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ModelAndView getLoginPage(@RequestParam Optional<String> error) {
        return new ModelAndView("login", "error", error);
    }

}

请注意,它只处理GET请求方法,通过error在模型中返回具有可选参数的视图。POST表单的部分和实际处理将由Spring Security完成。

表单的模板如下所示:

<form role="form" action="/login" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
    <div>
        <label for="email">Email address</label>
        <input type="email" name="email" id="email" required autofocus>
    </div>
    <div>
        <label for="password">Password</label>
        <input type="password" name="password" id="password" required>
    </div>
    <div>
        <label for="remember-me">Remember me</label>
        <input type="checkbox" name="remember-me" id="remember-me">
    </div>
    <button type="submit">Sign in</button>
</form>

<#if error.isPresent()>
<p>The email or password you have entered is invalid, try again.</p>
</#if>

它具有普通emailpassword输入字段,一个remember-me复选框和一个提交按钮。如果出现错误,将显示说明认证失败的消息。

这有效地包含了该应用程序功能所需的一切。剩下的是添加一些安全功能。

CSRF保护

关于_csrf上面窗体视图中出现的内容。它是由应用程序生成的用于验证请求的CSRF令牌。这是为了确保表单数据来自您的应用程序,而不是来自其他地方。这是Spring Security的一个功能,默认情况下由Spring Boot启动。

对于JSP和Freemarker,_csrf变量只是暴露在视图中。它包含一个CsrfToken对象,该对象具有一个getParameterName()方法来获取一个CSRF参数名称(默认情况下是这个名称_csrf)和一个getToken()获取实际令牌的方法。然后,您可以将其放在一个隐藏的领域以及其余的表单中:

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

认证

现在我们有一个应用程序准备好了,是时候设置身份验证。当我们使用我们的登录表单识别那个人,作为现有用户,并且获得足够的信息以授权他们的进一步请求,认证意味着这一部分。

组态

需要添加Spring Security的此配置:

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .formLogin()
                .loginPage("/login")
                .failureUrl("/login?error")
                .usernameParameter("email")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                .permitAll();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }

}

这肯定需要一个解释。

首先要提到的是@Order注释,它基本上保留了Spring Boot设置的所有默认值,只是在这个文件中覆盖它们。

configure(HttpSecurity http)方法是设置实际的基于URL的安全性。我们来看看它在这里做什么

  • 登录表格在下/login,允许所有。在登录失败时,/login?error会发生重定向。我们LoginController映射了这个URL。
  • 以登录形式保存用户名的参数称为“电子邮件”,因为这是我们用作用户名。
  • 注销URL是/logout允许的。之后,用户将被重定向到/。这里一个重要的意见是,如果CSRF保护开启,请求/logout应该是POST

configure(AuthenticationManagerBuilder auth)方法是设置认证机制的地方。它的设置使得认证将被处理UserDetailsService,哪个实现被注入,并且密码预期被加密BCryptPasswordEncoder

值得一提的是还有很多其他的认证方法UserDetailsService。这只是一种使我们能够使用现有的服务层对象来实现的方法,因此它很适合这个应用。

UserDetailsS​​ervice

UserDetailsService是Spring Security使用的接口,用于了解用户是否使用登录表单,他们的密码应该是什么,以及系统中有哪些权限。它有一个单一的方法:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

如果用户名存在,则该方法loadUserByUsername()返回UserDetails实例,如果不存在则返回实例UsernameNotFoundException

我的实现,注入到同一个SecurityConfig如下:

@Service
public class CurrentUserDetailsService implements UserDetailsService {
    private final UserService userService;

    @Autowired
    public CurrentUserDetailsService(UserService userService) {
        this.userService = userService;
    }

    @Override
    public CurrentUser loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userService.getUserByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException(String.format("User with email=%s was not found", email)));
        return new CurrentUser(user);
    }
}

正如你所看到的,只是要求UserService有一个电子邮件的用户。如果不存在,抛出异常。如果是,CurrentUser则返回对象。

但是呢CurrentUser?它应该是UserDetails。那么,首先,这UserDetails只是一个接口:

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

它描述了具有用户名,密码,GrantedAuthority对象列表以及某些不明确标志的用户,这些标志由于各种帐户无效的原因而被描述。

所以我们需要返回一个实现。至少有一个是Spring Security提供的org.springframework.security.core.userdetails.User

可以使用它,但棘手的部分是将我们的User域对象与UserDetails授权可能需要相关联。它可以通过多种方式完成:

  • 使User域对象UserDetails直接实现 - 它将允许返回User完全按照收到的方式UserService。缺点就是用与Spring Security相关的代码来“污染”域对象。
  • 使用提供的实现org.springframework.security.core.userdetails.User,只需将User实体映射到它。这很好,但是有一些关于用户可用的附加信息,如id直接访问role或其他任何东西是很好的。
  • 因此,第三个解决方案是扩展提供的实现,并添加可能需要的任何信息,或者只是一个完整的User对象。

最后一个选择是我在这里使用的,所以CurrentUser

public class CurrentUser extends org.springframework.security.core.userdetails.User {

    private User user;

    public CurrentUser(User user) {
        super(user.getEmail(), user.getPasswordHash(), AuthorityUtils.createAuthorityList(user.getRole().toString()));
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    public Long getId() {
        return user.getId();
    }

    public Role getRole() {
        return user.getRole();
    }

}

...扩展org.springframework.security.core.userdetails.User,实现UserDetails。此外,它只是包装我们的User域对象,加入(可选)方便的方法是代理给它(getRole()getId(),等)。

这个映射很简单,就是在构造函数中发生的:

  • UserDetails.username从填充User.email
  • UserDetails.password从填充User.passwordHash
  • User.role转换为String,由AuthorityUtils辅助类包装在GrantedAuthority对象中。它将作为列表的唯一元素UserDetails.getAuthorities()被调用时可用。
  • 我决定忘记这些标志,因为我没有使用它们,它们都是按照原样返回trueorg.springframework.security.core.userdetails.User

话虽如此,我必须提到,当你传递这样的包裹实体时,要小心,或者直接实现它们UserDetails。像这样泄漏域对象并不是一件正确的事情。这也可能是有问题的,即如果实体有一些LAZY获取的关联,那么在尝试获取Hibernate会话之外时可能遇到问题。

或者,只需从实体复制足够的信息CurrentUser。足够这里足以授权用户,而无需为User实体调用数据库,因为这将增加执行授权的成本。

无论如何,这应该使我们的登录表单工作。总而言之,一步一步地发生的是:

  • 用户打开/loginURL。
  • LoginController返回与登录形式的图。
  • 用户填写表单,提交。
  • Spring Security调用CurrentUserDetailsService.loadUserByUsername()用户名(在这种情况下为电子邮件)刚刚输入到表单中。
  • CurrentUserDetailsService从中获取用户UserDetailsService并返回CurrentUser
  • Spring Security调用CurrentUser.getPassword()并将密码哈希与表单中提供的散列密码进行比较。
  • 如果没关系,用户被重定向到他来的地方/login,如果不是,重定向到/login?error,再次处理LoginController
  • Spring Security'保持' CurrentUser对象(包裹Authentication)用于授权检查,或者您应该需要它。

记住我的身份验证

有时候,对于那些不想输入登录表单的懒惰者,即使他们的会话过期,也可以“记住我”身份验证。这样会造成安全隐患,所以只要建议您使用它。

它的工作原理如下:

  • 用户登录,表单发布一个remember-me参数(在此应用程序中有一个复选框)
  • Spring Security生成一个令牌,保存它,并将其发送给一个名为cookie的用户remember-me
  • 下一次访问应用程序时,Spring Security会查找该cookie,如果它保存有效和未过期的令牌,它将自动验证该用户。
  • 此外,您可以通过“记住我”来确定用户是否被认证。

为了启用它,它只需要一些补充SecurityConfig.configure(HttpSecurity http),所以它将看起来像这样:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .formLogin()
            .loginPage("/login")
            .failureUrl("/login?error")
            .usernameParameter("email")
            .permitAll()
            .and()
            .logout()
            .logoutUrl("/logout")
            .deleteCookies("remember-me")
            .logoutSuccessUrl("/")
            .permitAll()
            .and()
            .rememberMe();
}

注意rememberMe()到底。它使整个事情。值得注意的是,默认情况下,令牌存储在内存中。这意味着当应用程序重新启动时,令牌将丢失。你可以使它们持久化,即存储在数据库中,但对于这个应用程序来说,这是足够的。

另一项新的事情是deleteCookies()logout()remember-me一旦用户从应用程序中注销,就会强制删除该cookie。

授权

授权是一个过程,找出那个已经被授权的人,我们知道他的一切,可以访问应用程序提供的指定资源。

在这个应用程序中,我们将这些信息包含在一个CurrentUser对象中。从安全的角度来说,重要的是GrantedAuthority他所拥有的。你记得我们直接从User.role字段填充,所以他们将是“USER”或“ADMIN”。

基于URL的授权

现在,根据要求,我们希望所有人都可以访问某些网址,有些则由授权人员管理,还有一些由管理员访问。

它需要一些补充SecurityConfig

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/", "/public/**").permitAll()
            .antMatchers("/users/**").hasAuthority("ADMIN")
            .anyRequest().fullyAuthenticated()
            .and()
            .formLogin()
            .loginPage("/login")
            .failureUrl("/login?error")
            .usernameParameter("email")
            .permitAll()
            .and()
            .logout()
            .logoutUrl("/logout")
            .deleteCookies("remember-me")
            .logoutSuccessUrl("/")
            .permitAll()
            .and()
            .rememberMe();
}

新的东西是电话antMatchers()。这强制执行:

  • 网址匹配/模式(此应用中的主页)和/public/**所有匹配的网址都将被允许。
  • URL匹配/users/**模式(此应用程序中的列表视图)将仅允许具有“ADMIN”权限的用户。
  • 任何其他请求都需要经过身份验证的用户(“ADMIN”或“USER”)。

方法级授权

以上所有步骤都满足了大部分的要求,但是我们只能通过基于URL的授权无法解决。

那是:

  • 管理员用户可以列出并查看所有用户,并创建新的用户

这里的问题是,创建新用户的表单被映射到/user/create。我们可以添加这个URL SecurityConfig,但是如果我们做出一个习惯,我们最终会在配置文件中使用微型管理URL。我们也可以将其映射到/users/create这里,这可能在这里是有意义的,但是想到即将/user/{id}/edit来可能出现的那样。这是很好的离开它,因为它只是为了添加方法级别的授权给现有的方法UserController

要使方法级授权工作,@EnableGlobalMethodSecurity(prePostEnabled = true)需要将注释添加到SecurityConfig

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
class SecurityConfig extends WebSecurityConfigurerAdapter {
    // contents as before
}

这使您可以在应用程序的任何层中对公共方法使用两个注释:

  • @PreAuthorize("expression")- 如果expressionSpring表达式语言(Sprint)编写的,它将评估为“true”,方法将被调用。
  • @PostAuthorize("expression") - 首先调用该方法,当检查失败时,403状态返回给用户。

因此,为了解决第一个要求,处理用户创建的方法UserController需要注释@PreAuthorize

@PreAuthorize("hasAuthority('ADMIN')")
@RequestMapping(value = "/user/create", method = RequestMethod.GET)
public ModelAndView getUserCreatePage() {
    // contents as before
}

@PreAuthorize("hasAuthority('ADMIN')")
@RequestMapping(value = "/user/create", method = RequestMethod.POST)
public String handleUserCreateForm(@Valid @ModelAttribute("form") UserCreateForm form, BindingResult bindingResult) {
    // contents as before
}

hasAuthority()规划环境地政司表达是由Spring Security的提供等等,即:

  • hasAnyAuthority()或者hasAnyRole()(“权限”和“角色”是Spring Security lingo中的同义词!) - 检查当前用户是否具有列表中的GrantedAuthority之一。
  • hasAuthority()hasRole()- 如上所述,仅为一个。
  • isAuthenticated()isAnonymous()- 当前用户是否被认证。
  • isRememberMe()或者isFullyAuthenticated()- 当前用户是否通过“记住我”令牌进行身份验证。

域对象安全

最后的要求是这样的:

  • 非管理员用户可以查看他们的信息,但不能窥视其他用户

换句话说,我们需要一种方法来告诉用户什么时候可以请求/user/{id}。当用户是“ADMIN”(可以通过基于URL的身份验证来解决),而且当当前用户对自己发出请求时也是如此,这是不能解决的。

这适用于以下方法UserController

 @RequestMapping("/user/{id}")
 public ModelAndView getUserPage(@PathVariable Long id) {
     // contents as before
 }

我们需要检查当前用户是“管理” id传递给方法是一样的idUser与关联的域对象CurrentUser。由于CurrentUserUser实例包裹,我们有这方面的信息。所以我们可以用至少两种方法解决它:

  • 您可以使用Spel表达式来比较id传递给当前用户的方法id
  • 您可以将此检查委托给服务 - 这是首选的,因为将来可能会改变条件,更好地在一个位置进行更改,而不是在使用注释的任何位置。

要做到这一点,我CurrentUserService用这个界面创建了

public interface CurrentUserService {
    boolean canAccessUser(CurrentUser currentUser, Long userId);
}

而这个实现:

@Service
public class CurrentUserServiceImpl implements CurrentUserService {

    @Override
    public boolean canAccessUser(CurrentUser currentUser, Long userId) {
        return currentUser != null
                && (currentUser.getRole() == Role.ADMIN || currentUser.getId().equals(userId));
    }

}

现在在@PreAuthorize注释中使用它,只需放:

@PreAuthorize("@currentUserServiceImpl.canAccessUser(principal, #id)")
@RequestMapping("/user/{id}")
public ModelAndView getUserPage(@PathVariable Long id) {
    // contents as before
}

哪个是用于调用具有principal(CurrentUser)实例的服务的SpEL表达式,并将id参数传递给方法。

如果安全模型比较简单,这种方法可以正常工作。如果您发现自己写的东西类似于给予和检查多个访问权限,例如每个用户对多个域对象的查看/编辑/删除,那么最好是进入使用Spring Security Domain对象ACL的方向

这满足了所有的要求,此时应用程序得到了保障。

访问Spring Bean中的当前用户

无论何时需要从Controller或Service访问当前用户,都可以注入Authentication对象。可以通过调用获取当前的用户实例Authentication.getPrincipal()

这适用于构造函数或属性,如下所示:

@Autowired
private Authentication authentication;

void someMethod() {
    UserDetails currentUser = (UserDetails) authentication.getPrincipal();
}

此外,这适用于由@RequestMapping或者注释的控制器方法中的参数@ModelAttribute

@RequestMapping("/")
public String getMainPage(Authentication authentication) {
    UserDetails currentUser = (UserDetails) authentication.getPrincipal();
    // something
}

在这个应用程序中,可以直接转换CurrentUser,因为它UserDetails是正在使用的实际实现:

CurrentUser currentUser = (CurrentUser) authentication.getPrincipal();

这样你也可以访问它所包围的域对象。

访问视图中的当前用户

访问当前身份验证的用户在视图中是有用的,即当为具有一定权限的用户呈现UI的某些元素时。

在JSP中,可以使用安全标签库来完成,如下所示:

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<sec:authentication property="principal.username" />

这应该打印当前用户的名字。对于Freemarker,也可以使用该taglib,尽管它需要更多的功能。

您还可以拧紧taglib,并将Authentication对象作为视图的模型属性传递:

@RequestMapping("/")
public String getMainPage(@ModelProperty("authentication") Authentication authentication) {
    return "some_view";
}

但是为什么不为所有的观点,使用@ControllerAdvice,只是UserDetailsAuthentication

@ControllerAdvice
public class CurrentUserControllerAdvice {
    @ModelAttribute("currentUser")
    public UserDetails getCurrentUser(Authentication authentication) {
        return (authentication == null) ? null : (UserDetails) authentication.getPrincipal();
    }
}

之后,您可以通过currentUser所有视图中的属性访问它。

暂无评论