专门做二手手机的网站,怎么做dj网站,dedecms 网站地图,wordpress 文章页调用1 背景
实际应用系统中#xff0c;为了安全起见#xff0c;一般都必备用户认证#xff08;登录#xff09;和权限控制的功能#xff0c;以识别用户是否合法#xff0c;以及根据权限来控制用户是否能够执行某项操作。
Spring Security是一个安全相关的框架#xff0c;能…1 背景
实际应用系统中为了安全起见一般都必备用户认证登录和权限控制的功能以识别用户是否合法以及根据权限来控制用户是否能够执行某项操作。
Spring Security是一个安全相关的框架能够与Spring项目无缝整合本文主要是介绍Spring Security默认的用户认证和权限控制的使用方法和原理但不涉及到自定义实现。
Spring Security用户认证和权限控制自定义实现这篇文章专门讲解用户认证和权限控制相关的自定义实现。
2 实战示例
2.1 创建工程
创建一个名为authentication-server的spring boot工程项目结构如下图所示说明该spring boot工程主要是整合了Spring Security框架和Spring MVC框架。
2.2 配置说明
pom.xml配置文件如下所示
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdcom.authentication.server/groupIdartifactIdauthentication-server/artifactIdversion0.0.1-SNAPSHOT/versionnameauthentication-server/namedescription统一用户认证中心/descriptionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.1.3.RELEASE/versionrelativePath/ !-- lookup parent from repository --/parentpropertiesjava.version1.8/java.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/project2.2 用户认证
要想使用Spring Security框架配置类需要继承WebSecurityConfigurerAdapter类并通过注解EnableWebSecurity来启用Spring Security。
本文的用户认证是使用Spring Security默认的基于用户名和密码的表单认证需要在配置类中重写protected void configure(AuthenticationManagerBuilder auth)方法并在重写的方法中指定默认从哪里获取认证用户的信息即指定一个UserDetailsService接口的实现类。此外还需要重写protected void configure(HttpSecurity http)方法并在重写的方法中进行一系列的安全配置。本示例的配置类WebSecurityConfig代码如下所示
package com.authentication.server.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;/*** Spring Security配置类*/
EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {Autowiredprivate UserDetailsService userDetailsServiceImpl;/*** 用户认证配置*/Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {/*** 指定用户认证时默认从哪里获取认证用户信息*/auth.userDetailsService(userDetailsServiceImpl);}/*** Http安全配置*/Overrideprotected void configure(HttpSecurity http) throws Exception {/*** 表单登录使用默认的表单登录页面和登录端点/login进行登录* 退出登录使用默认的退出登录端点/logout退出登录* 记住我使用默认的“记住我”功能把记住用户已登录的Token保存在内存里记住30分钟* 权限除了/toHome和/toUser之外的其它请求都要求用户已登录* 注意Controller中也对URL配置了权限如果WebSecurityConfig中和Controller中都对某文化URL配置了权限则取较小的权限*/http.formLogin().defaultSuccessUrl(/toHome, false).permitAll().and().logout().permitAll().and().rememberMe().tokenValiditySeconds(1800).and().authorizeRequests().antMatchers(/toHome, /toUser).permitAll().anyRequest().authenticated();}/*** 密码加密器*/Beanpublic PasswordEncoder passwordEncoder() {/*** BCryptPasswordEncoder相同的密码明文每次生成的密文都不同安全性更高*/return new BCryptPasswordEncoder();}}Spring Security进行用户认证时需要根据用户的账号、密码、权限等信息进行认证因此需要根据查询到的用户信息封装成一个认证用户对象并交给Spring Security进行认证。查询用户信息并封装成认证用户对象的过程是在UserDetailsService接口的实现类需要用户自己实现中完成的。本示例的UserDetailsService接口实现类UserDetailsServiceImpl的代码如下所示
package com.authentication.server.service.impl;import com.authentication.server.model.AuthUser;
import com.authentication.server.service.AuthUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import java.util.ArrayList;
import java.util.List;/*** 自定义的认证用户获取服务类*/
Component(userDetailsServiceImpl)
public class UserDetailsServiceImpl implements UserDetailsService {Autowiredprivate AuthUserService authUserServiceImpl;/*** 根据用户名获取认证用户信息*/Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {if(StringUtils.isEmpty(username)) {throw new UsernameNotFoundException(UserDetailsService没有接收到用户账号);} else {/*** 根据用户名查找用户信息*/AuthUser authUser authUserServiceImpl.getAuthUserByUsername(username);if(authUser null) {throw new UsernameNotFoundException(String.format(用户%s不存在, username));}ListGrantedAuthority grantedAuthorities new ArrayList();for (String role : authUser.getRoles()) {//封装用户信息和角色信息到SecurityContextHolder全局缓存中grantedAuthorities.add(new SimpleGrantedAuthority(role));}/*** 创建一个用于认证的用户对象并返回包括用户名密码角色*/return new User(authUser.getUsername(), authUser.getPassword(), grantedAuthorities);}}
}查询用户信息的接口AuthUserService 的代码如下所示
package com.authentication.server.service;import com.authentication.server.model.AuthUser;/*** 用户服务类*/
public interface AuthUserService {/*** 通过用户账号获取认证用户信息*/AuthUser getAuthUserByUsername(String username);}查询用户信息的接口实现类AuthUserServiceImpl的代码如下所示
package com.authentication.server.service.impl;import com.authentication.server.model.AuthUser;
import com.authentication.server.service.AuthUserService;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;/*** 用户服务实现类*/
Service
public class AuthUserServiceImpl implements AuthUserService {/*** 通过用户账号获取用户信息*/Overridepublic AuthUser getAuthUserByUsername(String username) {/*** 实际上这里应该是从数据库中查询或者是调用其它服务接口获取* 为了方便这里直接创建用户信息* admin用户拥有 ROLE_ADMIN 和 ROLE_EMPLOYEE 这两个角色* employee用户拥有 ROLE_EMPLOYEE 这个角色* temp用户没有角色*/if(username.equals(admin)) {AuthUser user new AuthUser();user.setId(1L);user.setUsername(admin);/*** 密码为123通过BCryptPasswordEncoderl加密后的密文*/user.setPassword($2a$10$U6g06YmMfRJXcNfLP28TR.xy21u1A5kIeY/OZMKBDVMbn7PGJiaZS);ListString roles new ArrayList();roles.add(ROLE_ADMIN);roles.add(ROLE_EMPLOYEE);user.setRoles(roles);return user;} else if(username.equals(employee)) {AuthUser user new AuthUser();user.setId(2L);user.setUsername(employee);/*** 密码为123通过BCryptPasswordEncoderl加密后的密文*/user.setPassword($2a$10$U6g06YmMfRJXcNfLP28TR.xy21u1A5kIeY/OZMKBDVMbn7PGJiaZS);ListString roles new ArrayList();roles.add(ROLE_EMPLOYEE);user.setRoles(roles);return user;} else if (username.equals(temp)) {AuthUser user new AuthUser();user.setId(3L);user.setUsername(temp);/*** 密码为123通过BCryptPasswordEncoderl加密后的密文*/user.setPassword($2a$10$U6g06YmMfRJXcNfLP28TR.xy21u1A5kIeY/OZMKBDVMbn7PGJiaZS);ListString roles new ArrayList();user.setRoles(roles);return user;} else {return null;}}}用户信息实体类如下所示
package com.authentication.server.model;import java.util.List;/*** 用户实体类*/
public class AuthUser {/** 用户ID */private Long id;/** 用户账号 */private String username;/** 账号密码 */private String password;/** 角色集合 */private ListString roles;public Long getId() {return id;}public void setId(Long id) {this.id id;}public String getUsername() {return username;}public void setUsername(String username) {this.username username;}public String getPassword() {return password;}public void setPassword(String password) {this.password password;}public ListString getRoles() {return roles;}public void setRoles(ListString roles) {this.roles roles;}
}用户认证成功之后可以通过AuthenticationPrincipal注解来获取认证用户信息本示例中获取认证用户信息的web入口类UserController的代码如下所示
package com.authentication.server.controller;import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import java.security.Principal;/*** 用户接口类返回JSON*/
RestController
public class UserController {/*** 获取登录后的Principal需要登录*/GetMapping(/getPrincipal)public Object getPrincipal(AuthenticationPrincipal Principal principal){return principal;}/*** 获取登录后的UserDetails需要登录*/GetMapping(/getUserDetails)public Object getUserDetails(AuthenticationPrincipal UserDetails userDetails) {return userDetails;}}2.3 权限控制
Spring Security提供了默认的权限控制功能需要预先分配给用户特定的权限并指定各项操作执行所要求的权限。用户请求执行某项操作时Spring Security会先检查用户所拥有的权限是否符合执行该项操作所要求的权限如果符合才允许执行该项操作否则拒绝执行该项操作。
本示例中使用的是Spring Security提供的方法级别的权限控制即根据权限来控制用户是否能够请求某个方法。首先需要在工程的主启动类中使用注解EnableGlobalMethodSecurity(prePostEnabled true)来启动方法级别的权限控制并指定是在方法执行之前进行权限验证然后需要在方法的入口处通过注解PreAuthorize()来指定执行对应方法需要什么样的权限。
本示例的主启动类AuthenticationServerApplication的代码如下所示
package com.authentication.server;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;/*** 主启动类*/
ComponentScan(com.authentication.server.*)
EnableGlobalMethodSecurity(prePostEnabled true)
SpringBootApplication
public class AuthenticationServerApplication {public static void main(String[] args) {SpringApplication.run(AuthenticationServerApplication.class, args);}}
本示例中使用的是基于角色的权限控制即验证用户所拥有的角色是否符合执行某个方法所需要的角色如果符合才允许执行该方法否则拒绝执行该方法。需要在方法入口处通过注解 PreAuthorize(“hasRole(‘角色名称’)”)来指定执行对应方法需要什么角色并且是在执行对应方法之前进行角色验证。
本示例的方法入口控制类PageController的代码如下所示
package com.authentication.server.controller;import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;/*** 页面接口类(页面跳转)*/
Controller
public class PageController {/*** 跳转到admin.html页面需要登录且需要ROLE_ADMIN角色*/GetMapping(/toAdmin)PreAuthorize(hasRole(ROLE_ADMIN))public String toAdmin() {return admin.html;}/*** 跳转到employee.html页面需要登录且需要ROLE_EMPLOYEE角色*/GetMapping(/toEmployee)PreAuthorize(hasRole(ROLE_EMPLOYEE))public String toEmployee() {return employee.html;}/*** 跳转到employee.html页面需要登录但不需要角色* 注意虽然WebSecurityConfig中配置了/toUser不需要登录但是这里配置的权限更小因此/toUser以这里的配置为准*/GetMapping(/toUser)PreAuthorize(isAuthenticated())public String toUser() {return user.html;}/*** 跳转到home.html页面需要登录但不需要角色* 注意虽然这里配置了/toAbout不需要登录但WebSecurityConfig中配置的权限更小因此/toAbout以WebSecurityConfig中配置的为准*/RequestMapping(/toAbout)PreAuthorize(permitAll)public String toAbout() {return about.html;}/*** 跳转到home.html页面不需要登录*/RequestMapping(/toHome)public String toHome() {return home.html;}}静态页面admin.html的代码如下所示
!DOCTYPE html
html langen
headmeta charsetUTF-8titleadmin页面/title
/head
bodyh1这是Admin页面需要登录且需要ROLE_ADMIN角色/h1
/body
/html静态页面employee.html、user.html、home.html、about.html的代码与admin.html的相似就不一一展示。
3 功能测试
3.1 用户认证功能测试
运行AuthenticationServerApplication主启动类以启动项目然后通过浏览器访问以下地址请求toUser()方法即访问user.html页面由于toUser()方法需要用户经过认证之后才能访问因此会自动跳转到用户认证页面如下图所示进行认证
http://localhost:8080/toUser输入用户名admin和密码123并点击Sign in按钮认证成功后会自动请求/toHome路径并跳转到自定义的认证成功跳转页面home.hmtl如下图所示 通过浏览器访问以下地址可以查看到认证用户信息如下图所示
http://localhost:8080/getPrincipal
http://localhost:8080/getUserDetails 3.2 “记住我” 功能测试
用户进行认证时如果勾选了用户认证页面中的Remember me on this computer选项则当用户关闭浏览器之后系统会记住该用户一段时间由设置的有效期决定本示例中是1800秒如果在这段时间之内当用户重新访问该系统时用户不需要重新进行认证就已经是已认证的状态。
使用用户名admin和密码123进行用户认证时勾选上用户认证页面中的Remember me on this computer选项然后重复执行关闭浏览器后再访问以下地址并观察关闭了浏览器之后再重新访问时是跳转到认证页面未认证状态还是直接跳转到了user.html页面已认证状态
http://localhost:8080/toUser
3.3 退出功能测试
用户认证成功之后用户就处于已认证的状态就可以在权限之内访问系统此时可以通过访问以下地址请求退出已认证状态
http://localhost:8080/logout
首先会跳转到退出确认页面如下图所示用户点击了Log Out按钮之后才会真正的执行退出操作即回到未认证状态。
3.4 权限控制功能测试
用户在未认证的情况下通过浏览器访问以下地址请求toAdmin()方法即访问admin.html页面由于该方法要求用户已认证且具有ROLE_ADMIN权限才能访问因此会自动跳转到用户认证页面。
http://localhost:8080/toAdmin
此时如果用用户名employee和密码123进行认证认证成功之后由于该用户没有ROLE_ADMIN权限因此会自动跳转到没有权限的页面如下图所示 如果用用户名admin和密码123进行认证认证成功之后由于该用户拥有ROLE_ADMIN权限因此会自动跳转到admin.html页面如下图所示 可见确实起到了权限控制的作用。
4 原理分析
4.1 用户认证的过滤器链
Spring Security的用户认证流程是由一系列的过滤器链来实现的默认的关于用户认证的过滤器链大致如下图所示 SecurityContextPersistenceFilter 在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext并设置给 SecurityContextHolder。在请求完成后把 SecurityContextHolder 所持有的SecurityContext 保存到配置好的 SecurityContextRepository同时清除 securityContextHolder 所持有的 SecurityContext。 UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供用户名和密码其内部还有登录成功或失败后的处理器 AuthenticationSuccessHandler 和 AuthenticationFailureHandler。 ExceptionTranslationFilter 能够捕获过滤器链中产生的所有异常但只处理两类异常AuthenticationException 和 AccessDeniedException而其它的异常则继续抛出。 如果捕获到的是 AuthenticationException那么将会使用其对应的 AuthenticationEntryPoint 的commence()方法进行处理。在处理之前ExceptionTranslationFilter会先使用 RequestCache 将当前的HttpServerletRequest的信息保存起来以至于用户登录成功后可以跳转到之前的界面。 如果捕获到的是 AccessDeniedException那么将会根据当前访问的用户是否已经登录认证而做不同的处理如果未登录则使用关联的 AuthenticationEntryPoint 的 commence()方法进行处理否则使用关联的 AccessDeniedHandler 的handle()方法进行处理。 FilterSecurityInterceptor 用于保护HTTP资源的它需要一个 AuthenticationManager 和一个 AccessDecisionManager 的引用。它会从 SecurityContextHolder 中获取 Authentication然后通过 SecurityMetadataSource 可以得知当前请求是否在请求受保护的资源。对于请求那些受保护的资源如果 Authentication.isAuthenticated() 返回false即用户未认证或者FilterSecurityInterceptor 的 alwaysReauthenticate 属性的值为 true那么将会使用其引用的 AuthenticationManager 对Authentication进行认证认证之后再使用认证后的 Authentication 替换 SecurityContextHolder 中原有的那个。然后使用 AccessDecisionManager 对用户当前请求进行权限检查。
4.2 用户认证的流程
Spring Security支持多种用户认证的方式最常用的是基于用户名和密码的用户认证方式其认证流程如下图所示
4.3 “记住我” 功能的流程
用户可以使用账号和密码进行认证但是如果用户使用账号和密码进行认证时选择了“记住我”功能则在有效期内当用户关闭浏览器后再重新访问服务时不需要用户再次输入账号和密码重新进行认证而是通过“记住我”功能自动认证。
“记住我”功能的认证流程如下图所示 上述的用户认证处理逻辑都是基于Spring Security提供的默认实现我们只需要自己实现一个UserDetailsService接口用于获取用户认证信息即可十分简便。当然Spring Security也能够支持我们使用自定义的用户认证处理逻辑我们可以自己实现AuthenticationFilter和AuthenTicationProvider以达到按照需求进行用户认证的目的。博主的另外一篇文章会专门分享自定义用户认证的实现。
4.4 权限控制的原理
Spring Security允许我们通过Spring EL权限验证表达式来指定访问URL或方法所需要的权限用户在访问某个URL或方法时如果对应的权限验证表达式返回结果为true则表示用户拥有访问该URL或方法的权限如果返回结果为false则表示没有权限。Spring Security为我们提供了以下的权限验证表达式
表达式描述hasRole([role])当前用户是否拥有指定角色。hasAnyRole([role1,role2])多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。hasAuthority([auth])等同于hasRolehasAnyAuthority([auth1,auth2])等同于hasAnyRolePrinciple代表当前用户的principle对象authentication直接从SecurityContext获取的当前Authentication对象permitAll总是返回true表示允许所有的denyAll总是返回false表示拒绝所有的isAnonymous()当前用户是否是一个匿名用户isRememberMe()表示当前用户是否是通过Remember-Me自动登录的isAuthenticated()表示当前用户是否已经登录认证成功了。isFullyAuthenticated()如果当前用户既不是一个匿名用户同时又不是通过Remember-Me自动登录的则返回true。
权限验证表达式只能验证用户是否具有访问某个URL或方法的权限但是权限验证的这个步骤可以在不同的阶段进行。Spring Security中定义了以下四个支持使用权限验证表达式的注解其中前两者可以用来在方法调用前或者调用后进行权限验证后两者可以用来对集合类型的参数或者返回值进行过滤
PreAuthorizePostAuthorizePreFilterPostFilter
权限验证表达式需要和注解结合使用示例如下所示
PreAuthorize(hasRole(ROLE_ADMIN))
public void addUser(User user) {...
}PreAuthorize(hasRole(ROLE_USER) or hasRole(ROLE_ADMIN))
public User find(int id) {return null;
}PreAuthorize(#id10)
public User find(int id) {return null;
}PreAuthorize(principal.username.equals(#username))
public User find(String username) {return null;
}PreAuthorize(#user.name.equals(abc))
public void add(User user) {...
}PostAuthorize(returnObject.id%20)
public User find(int id) {...return user;
}PostFilter(filterObject.id%20)
public ListUser findAll() {ListUser userList new ArrayListUser();...return userList;
}PreFilter(filterTargetids, valuefilterObject%20)
public void delete(ListInteger ids, ListString usernames) {...
}5 总结
本文从使用方法和原理分析这两个方法简要的介绍了Spring Security的用户认证和权限控制这两大功能但都是基于Spring Security的默认实现我们也可以自定义用户认证和权限控制的实现逻辑Spring Security用户认证和权限控制自定义实现详细介绍了用户认证相关和权限控制相关的自定义实现。关于授权服务器、资源服务器的内容可以查阅以下几篇文章
OAuth2授权服务器和四种授权方式 这篇文章介绍了授权服务器和四种授权方式的配置与使用方法。OAuth2资源服务器 这篇文章介绍了基于方法级别的权限控制的资源服务器的配置与使用方法。