ASP.NET Core Identity 拥有完整的的用户认证、角色以及授权、开放认证的接口规范, 并且默认使用自家的 EntityFramework 进行了实现。
NHibernate 是 .NET 平台上老牌的对象关系映射 (ORM) 类库, 成熟度很高, 也实现了 ASP.NET Core Identity 的认证支持。
Identity 定义了一套完善的、可扩展的数据表结构, 存储用户、角色、权限等信息, 以及一套完善的用户/角色/权限管理 API 。
根据 NHibernate.AspNetCore.Identity 中的说明, 创建一个示例项目, 需要注意的问题主要有:
使用 Identity 创建用户 admin 的示例代码如下:
var user = await userManager.FindByNameAsync("admin");
if (user == null) {
user = new AppUser {
UserName = "admin",
Email = "admin@local.com",
EmailConfirmed = true,
PhoneNumber = "13400000000",
PhoneNumberConfirmed = true,
LockoutEnabled = true
};
await userManager.CreateAsync(user);
await userManager.AddPasswordAsync(user, "1a2b3c$D");
}
else {
var token = await userManager.GeneratePasswordResetTokenAsync(user);
await userManager.ResetPasswordAsync(user, token, "1a2b3c$D");
}
用户登录的示例代码为:
[HttpPost("login")]
public async Task<ActionResult<string>> Login(LoginModel model) {
var user = await userManager.FindByNameAsync(model.Username);
if (user == null) {
return NotFound("Not found!");
}
var isValid = await userManager.CheckPasswordAsync(user, model.Password);
if (isValid) {
await signInManager.SignInAsync(user, model.RememberMe);
return Ok(model.Username);
}
return "Invalid User!";
}
获取用户信息的示例代码为:
[HttpGet("info")]
public string GetInfo() {
return User.Identity.IsAuthenticated
? User.Identity.Name
: "anonymous";
}
对于熟悉 .NET 的开发者来说, 这些都是常规操作, 具体的示例项目代码可以参考这里 https://github.com/beginor/spring-identity-demo/tree/main/identity-web-demo 。
接下来就是本文的重点, 在 Spring 应用中使用 ASP.NET Identity 的数据库用户。
Spring Security 是 Spring 全家桶中负责认证的组件, 自然是 Spring 项目进行安全认证的首选。
访问 https://start.spring.io/ , 创建一个 Spring Web 应用, 本文的选择为:
添加的依赖项为:
下载并解压生成的项目, 输入命令 ./gradlew bootJar
, 等待编译完成。
在 application.yml 中添加数据源信息, 和上文的 .NET 应用的数据库信息保持一致:
spring:
datasource:
url: jdbc:postgresql://127.0.0.1:5432/spring_demo
username: postgres
password: postgis_11
driver-class-name: org.postgresql.Driver
创建一个自定义的 Sha256PasswordEncoder
进行密码存储, 代码如下:
public class Sha256PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
try {
var digest = MessageDigest.getInstance("SHA-256");
var hash = digest.digest(rawPassword.toString().getBytes(StandardCharsets.UTF_8));
var result = Base64.getEncoder().encodeToString(hash);
return result;
}
catch (Exception ex) {
return null;
}
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
var encoded = encode(rawPassword);
if (encoded == null) {
return false;
}
return encoded.equals(encodedPassword);
}
}
创建自定义的安全配置, 设置 JdbcDaoImpl
来查询 Identity 数据库中的信息, 代码如下:
@Configuration
@EnableWebSecurity()
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private DataSource dataSource;
/// 注入配置的数据源
@Autowired
public WebSecurityConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 基本的安全配置
http.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
// 使用内置的 JdbcDaoImpl 作为 UserDetailsService 。
var jdbcDao = new JdbcDaoImpl();
jdbcDao.setDataSource(dataSource);
// 禁用对单个用户的直接权限
jdbcDao.setEnableAuthorities(false);
// 启用用户组(角色)权限
jdbcDao.setEnableGroups(true);
jdbcDao.setUsernameBasedPrimaryKey(false);
// 从 aspnet_users 表中查询用户信息
jdbcDao.setUsersByUsernameQuery(
"select user_name as username, password_hash as password, email_confirmed as enabled\n" +
"from public.aspnet_users\n" +
"where normalized_user_name = upper(?);"
);
// 从 aspnet_role_claims 中查询用户所在角色的权限列表
jdbcDao.setGroupAuthoritiesByUsernameQuery(
"select r.id, r.name as group_name, rc.claim_value as authority\n" +
"from public.aspnet_roles r\n" +
"inner join public.aspnet_role_claims rc\n" +
" on rc.role_id = r.id and rc.claim_type = 'AppPrivilege'\n" +
"inner join public.aspnet_user_roles ur on ur.role_id = r.id\n" +
"inner join public.aspnet_users u on ur.user_id = u.id\n" +
" and u.normalized_user_name = upper(?);"
);
return jdbcDao;
}
// 使用自定义的 Sha256PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new Sha256PasswordEncoder();
}
}
要获取用户信息, 可以直接使用 Authentication
以获取用户信息, 代码如下
@GetMapping("/info")
public String getInfo(Authentication authentication) {
SecurityExpressionRoot root;
UserDetails user = (UserDetails) authentication.getPrincipal();
return "Hello, " + authentication.getName();
}
要了解更详细的实现信息, 请查看 security-web-demo 源代码。
Apache Shiro是一个功能强大且易于使用的Java安全框架, 很多 Spring 项目会选择 Shiro 作为安全认证。
访问 https://start.spring.io/ , 创建一个 Spring Web 应用, 本文的选择为:
添加的依赖项为:
下载并解压生成的项目, 输入命令 ./gradlew bootJar
, 等待编译完成。
根据 Shiro 的文档, 在 build.gradle 中添加依赖项:
implementation 'org.apache.shiro:shiro-spring-boot-web-starter:1.7.1'
在 application.yml 中添加数据源信息, 和上文的 .NET 应用的数据库信息保持一致:
spring:
datasource:
url: jdbc:postgresql://127.0.0.1:5432/spring_demo
username: postgres
password: postgis_11
driver-class-name: org.postgresql.Driver
根据 Shiro 的文档, 需要配置 Realm
和 ShiroFilterChainDefinition
, Shiro 提供了内置的 JdbcRealm
, 在这里调整为查询上面 .NET 应用创建的数据表, 并且使用相同的 SHA-256 对密码进行散列存储。
JdbcRealm
的 authenticationQuery
查询 aspnet_users
表中的用户信息;JdbcRealm
的 userRolesQuery
查询 aspnet_roles
表中的角色信息;JdbcRealm
的 permissionsQuery
查询 aspnet_role_claims
表中的角色权限信息;代码如下:
@Bean
public Realm realm(DataSource dataSource) {
var realm = new JdbcRealm();
realm.setDataSource(dataSource);
// 查询 aspnet_users 中的 password_hash 作为 password 返回
realm.setAuthenticationQuery(
"select password_hash as password " +
"from public.aspnet_users where user_name = ?"
);
// 查询 aspnet_roles 表中的角色名称;
realm.setUserRolesQuery(
"select r.name as role_name from public.aspnet_roles r " +
"inner join public.aspnet_user_roles ur on ur.role_id = r.id " +
"inner join public.aspnet_users u on u.id = ur.user_id " +
"where u.normalized_user_name = upper(?)"
);
// 启用权限查询
realm.setPermissionsLookupEnabled(true);
// 从 aspnet_role_claims 查询角色的权限
realm.setPermissionsQuery(
"select claim_value as permission from public.aspnet_role_claims rc " +
"inner join public.aspnet_roles r on r.id = rc.role_id " +
"and rc.claim_type = 'AppPrivilege' and r.name = ?"
);
// 使用 SHA-256 散列算法来加密存储密码
var matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("SHA-256");
matcher.setStoredCredentialsHexEncoded(false);
realm.setCredentialsMatcher(matcher);
return realm;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
var chainDef = new DefaultShiroFilterChainDefinition();
// chainDef.addPathDefinition("/**", "authc");
return chainDef;
}
使用 Shiro 进行登录的代码为:
@PostMapping("/login")
public String login(
@RequestBody LoginInfo info
) {
try {
var user = SecurityUtils.getSubject();
var token = new UsernamePasswordToken(
info.getUsername(),
info.getPassword(),
info.isRememberMe()
);
user.login(token);
return user.getPrincipal().toString();
}
catch (AuthenticationException ex) {
return ex.getMessage();
}
}
@GetMapping("/info")
public String getInfo() {
var user = SecurityUtils.getSubject();
return user.isAuthenticated()
? user.getPrincipal().toString()
: "anonymous";
}
要了解更详细的实现信息, 请查看 shiro-web-demo 源代码。
经过上面的折腾, 在数据库层面基本上统一了 .NET 和 Spring 应用的认证, 使用相同的数据库, 保护企业现有的资产, 比如使用原来的 .NET 后台管理用户、 角色、 权限、 菜单以及相互绑定, 而在新开发的 Spring 应用中直接使用这些信息。