spring boot 整合spring security采用mongodb数据库方式<p>案例中有使用mongodb数据库,具体的spring boot整合mongodb的案例参考地址<a href="http://www.leftso.com/blog/135.html" rel="nofollow" target="_blank">http://www.leftso.com/blog/135.html</a><br />
<br />
<a href="http://projects.spring.io/spring-security/" rel="nofollow" target="_blank">Spring Security</a>有一些使用复杂的意见。那当然,当你看的时候,它的范围很复杂,因为它的范围涵盖了大量的用例。事实上,真正的spring的精神,你不必一次使用你所拥有的用例的一切功能。事实上,当你开始使用<a href="http://projects.spring.io/spring-boot/" rel="nofollow" target="_blank">Spring Boot</a>进行樱桃挑选并将其恢复时,它似乎并不复杂。</p>
<p>我们从使用情况开始,我在想一些比较常见的东西,几乎每个项目都出现一些基本的访问限制。所以,这样一个应用程序的要求可以是:</p>
<ul>
<li>该应用将有用户,每个用户角色为管理员或用户</li>
<li>他们通过电子邮件和密码登录</li>
<li>非管理员用户可以查看他们的信息,但不能窥视其他用户</li>
<li>管理员用户可以列出并查看所有用户,并创建新的用户</li>
<li>定制表单登录</li>
<li>“记住我”验证懒惰</li>
<li>注销的可能性</li>
<li>主页将提供给所有人,不经过验证</li>
</ul>
<p>这样的<a href="https://github.com/bkielczewski/example-spring-boot-security" rel="nofollow" target="_blank">应用程序</a>的<a href="https://github.com/bkielczewski/example-spring-boot-security" rel="nofollow" target="_blank">源代码在GitHub上</a>,供您查看。建议您将源代码打开,以下将是某些关键点的评论。啊,和安全相关的东西接近尾声,所以只要知道基础知识就可以向下滚动。</p>
<h4>依赖关系</h4>
<p>此外,标准的Spring Boot依赖关系,最重要的依赖关系是Spring Security,Spring Data JPA的启动器模块,因为我们需要某处存储用户,而嵌入式内存中的HSQLDB作为存储引擎。</p>
<pre>
<code class="language-xml"><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>
</code></pre>
<p>在现实生活中,您可能会更有兴趣连接到某种外部数据库引擎,例如<a href="http://kielczewski.eu/2014/05/database-connection-pooling-with-bonecp-in-spring-boot-application/" rel="nofollow" target="_blank">本文中关于使用BoneCP的数据库连接池的描述</a>。我也使用<a href="http://freemarker.org/" rel="nofollow" target="_blank">Freemarker</a>作为模板引擎,但是如果这不是你的事情,那么对于JSP来说,重写它也应该很简单,就像在<a href="http://kielczewski.eu/2014/04/spring-boot-mvc-application/" rel="nofollow" target="_blank">本文中关于Spring MVC应用程序一样</a>。</p>
<h4>域模型</h4>
<p>所以我们会有这样的<code>User</code>实体:</p>
<pre>
<code class="language-java">@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
}
</code></pre>
<p>如您所见,只有密码的哈希将存储在数据库中,这通常是一个好主意。该<code>email</code>领域还有一个独特的约束,但它不是主要的关键。由于<code>id</code>某个原因,电子邮件地址是您不想在访问日志中显示的相当敏感的信息,因此用户被标识,所以我们将尽可能地使用id。</p>
<p><code>Role</code> 是一个简单的枚举:</p>
<pre>
<code class="language-java">public enum Role {
USER, ADMIN
}
</code></pre>
<p>除此之外,创建新用户的表单将是很好的:</p>
<pre>
<code class="language-java">public class UserCreateForm {
@NotEmpty
private String email = "";
@NotEmpty
private String password = "";
@NotEmpty
private String passwordRepeated = "";
@NotNull
private Role role = Role.USER;
}
</code></pre>
<p>这将用作Web层和服务层之间的数据传输对象(DTO)。它由Hibernate Validator验证约束注释,并设置一些合理的默认值。请注意,它与<code>User</code>对象略有不同,因此我希望将<code>User</code>实体“泄漏” 到Web层中,我真的不能。</p>
<p>这就是我们现在所需要的。</p>
<h4>服务层</h4>
<p>在服务层,业务逻辑应该是什么,我们需要一些东西来检索<code>User</code>他的id,电子邮件,列出所有的用户并创建一个新的用户。</p>
<p>所以接口将是:</p>
<pre>
<code class="language-java">public interface UserService {
Optional<User> getUserById(long id);
Optional<User> getUserByEmail(String email);
Collection<User> getAllUsers();
User create(UserCreateForm form);
}
</code></pre>
<p>服务的实现:</p>
<pre>
<code class="language-java">@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);
}
}
</code></pre>
<p>这里不值得一个评论 - 服务代理到<code>UserRepository</code>大部分时间。值得注意的是,在该<code>create()</code>方法中,该表单用于构建一个新<code>User</code>对象。哈希是从使用的密码<code>BCryptPasswordEncoder</code>生成的,这应该比臭名昭着的MD5产生更好的哈希。</p>
<p>的<code>UserRepository</code>定义如下:</p>
<pre>
<code class="language-java">public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findOneByEmail(String email);
}
</code></pre>
<p>这里只添加一个非默认方法<code>findOneByEmail</code>。请注意,我希望它返回<code>User</code>包装在JDK8中<code>Optional</code>,这是Spring的一个新功能,并且使处理<code>null</code>值更容易。</p>
<h4>Web层</h4>
<p>这是控制者及其观点。为了满足应用的要求,我们至少需要一对夫妇。</p>
<h4>主页</h4>
<p>我们将处理<code>/</code>网站的根<code>HomeController</code>,其中只返回一个<code>home</code>视图:</p>
<pre>
<code class="language-java">@Controller
public class HomeController {
@RequestMapping("/")
public String getHomePage() {
return "home";
}
}
</code></pre>
<h4>用户列表</h4>
<p>同样,用户的列表将被映射到<code>/users</code>并由其处理<code>UsersController</code>。它<code>UserService</code>注入,要求它返回<code>Collection</code>的<code>User</code>对象,将它们放入了<code>users</code>模型属性,然后调用<code>users</code>视图名称:</p>
<pre>
<code class="language-java">@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());
}
}
</code></pre>
<h4>查看和创建用户</h4>
<p>接下来,我们需要一个控制器来处理查看和创建一个新的用户,我称之为它<code>UserController</code>,它更复杂:</p>
<pre>
<code class="language-java">@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";
}
}
</code></pre>
<p>视图被映射到<code>/user/{id}</code>URL,由<code>getUserPage()</code>方法处理。它要求<code>UserService</code>一个用户<code>id</code>,它是从URL中提取的,并作为参数传递给此方法。你记得,<code>UserService.getUserById()</code>返回一个<code>User</code>包装的实例<code>Optional</code>。因此,<code>.orElseThrow()</code>被要求<code>Optional</code>获得一个<code>User</code> 实例,或者在它的时候抛出异常<code>null</code>。</p>
<p>创建一个新<code>User</code>的映射到<code>/user/create</code>并由两种方法处理:<code>getUserCreatePage()</code>和<code>handleUserCreateForm()</code>。第一个只返回一个<code>user_create</code>具有空格式的视图作为<code>form</code>模型的属性。另一个响应<code>POST</code>请求,并<code>UserCreateForm</code>作为参数进行验证。如果表单中有错误,则由<code>BindingResult</code>该视图返回。如果表单确定,则将其传递给<code>UserService.create()</code>方法。</p>
<p>还有一个额外的检查<code>DataIntegrityViolationException</code>。如果发生这种情况,则认为是因为尝试<code>User</code>使用数据库中已经存在的电子邮件地址来创建,因此再次呈现该表单。</p>
<p>在现实生活中,知道哪个约束被严重违反是非常困难的(或者与ORM无关),所以当做这些假设时,至少应该记录异常情况以便进一步检查。应该注意防止这种异常发生在第一位,如对于重复的电子邮件的形式的正确验证。</p>
<p>否则,如果一切正常,则重定向到<code>/users</code>URL。</p>
<h3>定制验证 <code>UserCreateForm</code></h3>
<p>该<code>@InitBinder</code>在注解的方法<code>UserController</code>添加<code>UserCreateFormValidator</code>到<code>form</code>参数,告诉它应该由它来验证。这样做的原因在于,<code>UserCreateForm</code>需要对两种可能的情况进行整体验证:</p>
<ul>
<li>检查密码和重复密码是否匹配</li>
<li>检查电子邮件是否存在于数据库中</li>
</ul>
<p>这样做,<code>UserCreateFormValidator</code>实现如下:</p>
<pre>
<code class="language-java">@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");
}
}
}
</code></pre>
<h4>在登录</h4>
<p>它将映射到<code>/login</code>URL并处理<code>LoginController</code>:</p>
<pre>
<code class="language-java">@Controller
public class LoginController {
@RequestMapping(value = "/login", method = RequestMethod.GET)
public ModelAndView getLoginPage(@RequestParam Optional<String> error) {
return new ModelAndView("login", "error", error);
}
}
</code></pre>
<p>请注意,它只处理<code>GET</code>请求方法,通过<code>error</code>在模型中返回具有可选参数的视图。<code>POST</code>表单的部分和实际处理将由Spring Security完成。</p>
<p>表单的模板如下所示:</p>
<pre>
<code class="language-html"><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>
</code></pre>
<p>它具有普通<code>email</code>,<code>password</code>输入字段,一个<code>remember-me</code>复选框和一个提交按钮。如果出现错误,将显示说明认证失败的消息。</p>
<p>这有效地包含了该应用程序功能所需的一切。剩下的是添加一些安全功能。</p>
<h4>CSRF保护</h4>
<p>关于<code>_csrf</code>上面窗体视图中出现的内容。它是由应用程序生成的用于验证请求的CSRF令牌。这是为了确保表单数据来自您的应用程序,而不是来自其他地方。这是Spring Security的一个功能,默认情况下由Spring Boot启动。</p>
<p>对于JSP和Freemarker,<code>_csrf</code>变量只是暴露在视图中。它包含一个<code>CsrfToken</code>对象,该对象具有一个<code>getParameterName()</code>方法来获取一个CSRF参数名称(默认情况下是这个名称<code>_csrf</code>)和一个<code>getToken()</code>获取实际令牌的方法。然后,您可以将其放在一个隐藏的领域以及其余的表单中:</p>
<pre>
<code class="language-html"><input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</code></pre>
<h4>认证</h4>
<p>现在我们有一个应用程序准备好了,是时候设置身份验证。当我们使用我们的登录表单识别那个人,作为现有用户,并且获得足够的信息以授权他们的进一步请求,认证意味着这一部分。</p>
<h4>组态</h4>
<p>需要添加Spring Security的此配置:</p>
<pre>
<code class="language-java">@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());
}
}
</code></pre>
<p>这肯定需要一个解释。</p>
<p>首先要提到的是<code>@Order</code>注释,它基本上保留了Spring Boot设置的所有默认值,只是在这个文件中覆盖它们。</p>
<p>该<code>configure(HttpSecurity http)</code>方法是设置实际的基于URL的安全性。我们来看看它在这里做什么</p>
<ul>
<li>登录表格在下<code>/login</code>,允许所有。在登录失败时,<code>/login?error</code>会发生重定向。我们<code>LoginController</code>映射了这个URL。</li>
<li>以登录形式保存用户名的参数称为“电子邮件”,因为这是我们用作用户名。</li>
<li>注销URL是<code>/logout</code>允许的。之后,用户将被重定向到<code>/</code>。这里一个重要的意见是,如果CSRF保护开启,请求<code>/logout</code>应该是<code>POST</code>。</li>
</ul>
<p>该<code>configure(AuthenticationManagerBuilder auth)</code>方法是设置认证机制的地方。它的设置使得认证将被处理<code>UserDetailsService</code>,哪个实现被注入,并且密码预期被加密<code>BCryptPasswordEncoder</code>。</p>
<p>值得一提的是还有很多其他的认证方法<code>UserDetailsService</code>。这只是一种使我们能够使用现有的服务层对象来实现的方法,因此它很适合这个应用。</p>
<h3>UserDetailsService</h3>
<p>这<code>UserDetailsService</code>是Spring Security使用的接口,用于了解用户是否使用登录表单,他们的密码应该是什么,以及系统中有哪些权限。它有一个单一的方法:</p>
<pre>
<code class="language-java">public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
</code></pre>
<p>如果用户名存在,则该方法<code>loadUserByUsername()</code>返回<code>UserDetails</code>实例,如果不存在则返回实例<code>UsernameNotFoundException</code>。</p>
<p>我的实现,注入到同一个<code>SecurityConfig</code>如下:</p>
<pre>
<code class="language-java">@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);
}
}
</code></pre>
<p>正如你所看到的,只是要求<code>UserService</code>有一个电子邮件的用户。如果不存在,抛出异常。如果是,<code>CurrentUser</code>则返回对象。</p>
<p>但是呢<code>CurrentUser</code>?它应该是<code>UserDetails</code>。那么,首先,这<code>UserDetails</code>只是一个接口:</p>
<pre>
<code class="language-java">public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
</code></pre>
<p>它描述了具有用户名,密码,<code>GrantedAuthority</code>对象列表以及某些不明确标志的用户,这些标志由于各种帐户无效的原因而被描述。</p>
<p>所以我们需要返回一个实现。至少有一个是Spring Security提供的<code>org.springframework.security.core.userdetails.User</code>。</p>
<p>可以使用它,但棘手的部分是将我们的<code>User</code>域对象与<code>UserDetails</code>授权可能需要相关联。它可以通过多种方式完成:</p>
<ul>
<li>使<code>User</code>域对象<code>UserDetails</code>直接实现 - 它将允许返回<code>User</code>完全按照收到的方式<code>UserService</code>。缺点就是用与Spring Security相关的代码来“污染”域对象。</li>
<li>使用提供的实现<code>org.springframework.security.core.userdetails.User</code>,只需将<code>User</code>实体映射到它。这很好,但是有一些关于用户可用的附加信息,如<code>id</code>直接访问<code>role</code>或其他任何东西是很好的。</li>
<li>因此,第三个解决方案是扩展提供的实现,并添加可能需要的任何信息,或者只是一个完整的<code>User</code>对象。</li>
</ul>
<p>最后一个选择是我在这里使用的,所以<code>CurrentUser</code>:</p>
<pre>
<code class="language-java">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();
}
}
</code></pre>
<p>...扩展<code>org.springframework.security.core.userdetails.User</code>,实现<code>UserDetails</code>。此外,它只是包装我们的<code>User</code>域对象,加入(可选)方便的方法是代理给它(<code>getRole()</code>,<code>getId()</code>,等)。</p>
<p>这个映射很简单,就是在构造函数中发生的:</p>
<ul>
<li>在<code>UserDetails.username</code>从填充<code>User.email</code></li>
<li>在<code>UserDetails.password</code>从填充<code>User.passwordHash</code></li>
<li>被<code>User.role</code>转换为<code>String</code>,由<code>AuthorityUtils</code>辅助类包装在GrantedAuthority对象中。它将作为列表的唯一元素<code>UserDetails.getAuthorities()</code>被调用时可用。</li>
<li>我决定忘记这些标志,因为我没有使用它们,它们都是按照原样返回<code>true</code>的<code>org.springframework.security.core.userdetails.User</code></li>
</ul>
<p>话虽如此,我必须提到,当你传递这样的包裹实体时,要小心,或者直接实现它们<code>UserDetails</code>。像这样泄漏域对象并不是一件正确的事情。这也可能是有问题的,即如果实体有一些LAZY获取的关联,那么在尝试获取Hibernate会话之外时可能遇到问题。</p>
<p>或者,只需从实体复制足够的信息<code>CurrentUser</code>。足够这里足以授权用户,而无需为<code>User</code>实体调用数据库,因为这将增加执行授权的成本。</p>
<p>无论如何,这应该使我们的登录表单工作。总而言之,一步一步地发生的是:</p>
<ul>
<li>用户打开<code>/login</code>URL。</li>
<li>的<code>LoginController</code>返回与登录形式的图。</li>
<li>用户填写表单,提交。</li>
<li>Spring Security调用<code>CurrentUserDetailsService.loadUserByUsername()</code>用户名(在这种情况下为电子邮件)刚刚输入到表单中。</li>
<li><code>CurrentUserDetailsService</code>从中获取用户<code>UserDetailsService</code>并返回<code>CurrentUser</code>。</li>
<li>Spring Security调用<code>CurrentUser.getPassword()</code>并将密码哈希与表单中提供的散列密码进行比较。</li>
<li>如果没关系,用户被重定向到他来的地方<code>/login</code>,如果不是,重定向到<code>/login?error</code>,再次处理<code>LoginController</code>。</li>
<li>Spring Security'保持' <code>CurrentUser</code>对象(包裹<code>Authentication</code>)用于授权检查,或者您应该需要它。</li>
</ul>
<h4>记住我的身份验证</h4>
<p>有时候,对于那些不想输入登录表单的懒惰者,即使他们的会话过期,也可以“记住我”身份验证。这样会造成安全隐患,所以只要建议您使用它。</p>
<p>它的工作原理如下:</p>
<ul>
<li>用户登录,表单发布一个<code>remember-me</code>参数(在此应用程序中有一个复选框)</li>
<li>Spring Security生成一个令牌,保存它,并将其发送给一个名为cookie的用户<code>remember-me</code>。</li>
<li>下一次访问应用程序时,Spring Security会查找该cookie,如果它保存有效和未过期的令牌,它将自动验证该用户。</li>
<li>此外,您可以通过“记住我”来确定用户是否被认证。</li>
</ul>
<p>为了启用它,它只需要一些补充<code>SecurityConfig.configure(HttpSecurity http)</code>,所以它将看起来像这样:</p>
<pre>
<code class="language-java">@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();
}
</code></pre>
<p>注意<code>rememberMe()</code>到底。它使整个事情。值得注意的是,默认情况下,令牌存储在内存中。这意味着当应用程序重新启动时,令牌将丢失。你可以使它们持久化,即存储在数据库中,但对于这个应用程序来说,这是足够的。</p>
<p>另一项新的事情是<code>deleteCookies()</code>对<code>logout()</code>。<code>remember-me</code>一旦用户从应用程序中注销,就会强制删除该cookie。</p>
<h4>授权</h4>
<p>授权是一个过程,找出那个已经被授权的人,我们知道他的一切,可以访问应用程序提供的指定资源。</p>
<p>在这个应用程序中,我们将这些信息包含在一个<code>CurrentUser</code>对象中。从安全的角度来说,重要的是<code>GrantedAuthority</code>他所拥有的。你记得我们直接从<code>User.role</code>字段填充,所以他们将是“USER”或“ADMIN”。</p>
<h4>基于URL的授权</h4>
<p>现在,根据要求,我们希望所有人都可以访问某些网址,有些则由授权人员管理,还有一些由管理员访问。</p>
<p>它需要一些补充<code>SecurityConfig</code>:</p>
<pre>
<code class="language-java">@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();
}
</code></pre>
<p>新的东西是电话<code>antMatchers()</code>。这强制执行:</p>
<ul>
<li>网址匹配<code>/</code>模式(此应用中的主页)和<code>/public/**</code>所有匹配的网址都将被允许。</li>
<li>URL匹配<code>/users/**</code>模式(此应用程序中的列表视图)将仅允许具有“ADMIN”权限的用户。</li>
<li>任何其他请求都需要经过身份验证的用户(“ADMIN”或“USER”)。</li>
</ul>
<h4>方法级授权</h4>
<p>以上所有步骤都满足了大部分的要求,但是我们只能通过基于URL的授权无法解决。</p>
<p>那是:</p>
<ul>
<li>管理员用户可以列出并查看所有用户,并创建新的用户</li>
</ul>
<p>这里的问题是,创建新用户的表单被映射到<code>/user/create</code>。我们可以添加这个URL <code>SecurityConfig</code>,但是如果我们做出一个习惯,我们最终会在配置文件中使用微型管理URL。我们也可以将其映射到<code>/users/create</code>这里,这可能在这里是有意义的,但是想到即将<code>/user/{id}/edit</code>来可能出现的那样。这是很好的离开它,因为它只是为了添加方法级别的授权给现有的方法<code>UserController</code>。</p>
<p>要使方法级授权工作,<code>@EnableGlobalMethodSecurity(prePostEnabled = true)</code>需要将注释添加到<code>SecurityConfig</code>:</p>
<pre>
<code class="language-java">@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
class SecurityConfig extends WebSecurityConfigurerAdapter {
// contents as before
}
</code></pre>
<p>这使您可以在应用程序的任何层中对公共方法使用两个注释:</p>
<ul>
<li><code>@PreAuthorize("expression")</code>- 如果<code>expression</code>以<a href="http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html" rel="nofollow" target="_blank">Spring表达式语言(Sprint)</a>编写的,它将评估为“true”,方法将被调用。</li>
<li><code>@PostAuthorize("expression")</code> - 首先调用该方法,当检查失败时,403状态返回给用户。</li>
</ul>
<p>因此,为了解决第一个要求,处理用户创建的方法<code>UserController</code>需要注释<code>@PreAuthorize</code>:</p>
<pre>
<code class="language-java">@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
}
</code></pre>
<p>该<code>hasAuthority()</code>规划环境地政司表达是由Spring Security的提供等等,即:</p>
<ul>
<li><code>hasAnyAuthority()</code>或者<code>hasAnyRole()</code>(“权限”和“角色”是Spring Security lingo中的同义词!) - 检查当前用户是否具有列表中的GrantedAuthority之一。</li>
<li><code>hasAuthority()</code>或<code>hasRole()</code>- 如上所述,仅为一个。</li>
<li><code>isAuthenticated()</code>或<code>isAnonymous()</code>- 当前用户是否被认证。</li>
<li><code>isRememberMe()</code>或者<code>isFullyAuthenticated()</code>- 当前用户是否通过“记住我”令牌进行身份验证。</li>
</ul>
<h4>域对象安全</h4>
<p>最后的要求是这样的:</p>
<ul>
<li>非管理员用户可以查看他们的信息,但不能窥视其他用户</li>
</ul>
<p>换句话说,我们需要一种方法来告诉用户什么时候可以请求<code>/user/{id}</code>。当用户是“ADMIN”(可以通过基于URL的身份验证来解决),而且当当前用户对自己发出请求时也是如此,这是不能解决的。</p>
<p>这适用于以下方法<code>UserController</code>:</p>
<pre>
<code class="language-java"> @RequestMapping("/user/{id}")
public ModelAndView getUserPage(@PathVariable Long id) {
// contents as before
}
</code></pre>
<p>我们需要检查当前用户是“管理” <strong>或</strong>在<code>id</code>传递给方法是一样的<code>id</code>从<code>User</code>与关联的域对象<code>CurrentUser</code>。由于<code>CurrentUser</code>有<code>User</code>实例包裹,我们有这方面的信息。所以我们可以用至少两种方法解决它:</p>
<ul>
<li>您可以使用Spel表达式来比较<code>id</code>传递给当前用户的方法<code>id</code>。</li>
<li>您可以将此检查委托给服务 - 这是首选的,因为将来可能会改变条件,更好地在一个位置进行更改,而不是在使用注释的任何位置。</li>
</ul>
<p>要做到这一点,我<code>CurrentUserService</code>用这个界面创建了</p>
<pre>
<code class="language-java">public interface CurrentUserService {
boolean canAccessUser(CurrentUser currentUser, Long userId);
}
</code></pre>
<p>而这个实现:</p>
<pre>
<code class="language-java">@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));
}
}
</code></pre>
<p>现在在<code>@PreAuthorize</code>注释中使用它,只需放:</p>
<pre>
<code class="language-java">@PreAuthorize("@currentUserServiceImpl.canAccessUser(principal, #id)")
@RequestMapping("/user/{id}")
public ModelAndView getUserPage(@PathVariable Long id) {
// contents as before
}
</code></pre>
<p>哪个是用于调用具有principal(<code>CurrentUser</code>)实例的服务的SpEL表达式,并将<code>id</code>参数传递给方法。</p>
<p>如果安全模型比较简单,这种方法可以正常工作。如果您发现自己写的东西类似于给予和检查多个访问权限,例如每个用户对多个域对象的查看/编辑/删除,那么最好是进入使用<a href="http://docs.spring.io/spring-security/site/docs/3.0.x/reference/domain-acls.html" rel="nofollow" target="_blank">Spring Security Domain对象ACL的方向</a>。</p>
<p>这满足了所有的要求,此时应用程序得到了保障。</p>
<h4>访问Spring Bean中的当前用户</h4>
<p>无论何时需要从Controller或Service访问当前用户,都可以注入<code>Authentication</code>对象。可以通过调用获取当前的用户实例<code>Authentication.getPrincipal()</code>。</p>
<p>这适用于构造函数或属性,如下所示:</p>
<pre>
<code class="language-java">@Autowired
private Authentication authentication;
void someMethod() {
UserDetails currentUser = (UserDetails) authentication.getPrincipal();
}
</code></pre>
<p>此外,这适用于由<code>@RequestMapping</code>或者注释的控制器方法中的参数<code>@ModelAttribute</code>:</p>
<pre>
<code class="language-java">@RequestMapping("/")
public String getMainPage(Authentication authentication) {
UserDetails currentUser = (UserDetails) authentication.getPrincipal();
// something
}
</code></pre>
<p>在这个应用程序中,可以直接转换<code>CurrentUser</code>,因为它<code>UserDetails</code>是正在使用的实际实现:</p>
<pre>
<code class="language-java">CurrentUser currentUser = (CurrentUser) authentication.getPrincipal();
</code></pre>
<p>这样你也可以访问它所包围的域对象。</p>
<h4>访问视图中的当前用户</h4>
<p>访问当前身份验证的用户在视图中是有用的,即当为具有一定权限的用户呈现UI的某些元素时。</p>
<p>在JSP中,可以使用安全标签库来完成,如下所示:</p>
<pre>
<code class="language-xml"><%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<sec:authentication property="principal.username" />
</code></pre>
<p>这应该打印当前用户的名字。对于Freemarker,也可以使用该taglib,尽管它需要更多的功能。</p>
<p>您还可以拧紧taglib,并将<code>Authentication</code>对象作为视图的模型属性传递:</p>
<pre>
<code class="language-java">@RequestMapping("/")
public String getMainPage(@ModelProperty("authentication") Authentication authentication) {
return "some_view";
}
</code></pre>
<p>但是为什么不为所有的观点,使用<code>@ControllerAdvice</code>,只是<code>UserDetails</code>从<code>Authentication</code>:</p>
<pre>
<code class="language-java">@ControllerAdvice
public class CurrentUserControllerAdvice {
@ModelAttribute("currentUser")
public UserDetails getCurrentUser(Authentication authentication) {
return (authentication == null) ? null : (UserDetails) authentication.getPrincipal();
}
}
</code></pre>
<p>之后,您可以通过<code>currentUser</code>所有视图中的属性访问它。</p>