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

位置:首页>文章>详情   分类: 教程分享 > Java教程   阅读(1910)   2024-04-17 12:31:24

案例中有使用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所有视图中的属性访问它。

标签:
地址:https://www.leftso.com/article/138.html

相关阅读

spring boot 整合spring security采用mongodb数据库方式
spring boot是一个崭新的spring框架分支项目,本文讲解基本的数据库配置
spring boot 入门之security oauth2 jwt完美整合例子,Java编程中spring boot框架+spring security框架+spring security o...
spring boot入门,spring boot是一个崭新的spring框架分支项目,本文讲解其属性配置相关
1.概述本文继续使用spring boot 和Spring Security系列进行注册,并着重于如何正确实现角色和权限
spring boot又一个spring框架的经典项目,本文讲解spring boot入门的环境配置以及第一个项目,Spring Boot 入门教程
spring boot 2.0 security 5.0 整合,实现自定义表单登录。spring boot 2.0框架使用。
spring boot 入门之spring session实现restful apis。通过spring boot或者spring mvc整合spring session的方式来实现sessio...
Spring Security 配置多个Authentication Providers认证器