首先加入spring security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
分别建立用户表和角色表
CREATE TABLE `admin` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`salt` varchar(10) NOT NULL,
`created` bigint(20) NOT NULL,
`last_login` bigint(20) NOT NULL,
`login_count` int(255) NOT NULL DEFAULT '0',
`enabled` tinyint(4) NOT NULL DEFAULT '0',
`locked` tinyint(4) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
CREATE TABLE `role` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`title` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
CREATE TABLE `admin_role` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`admin_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `for_admin_id` (`admin_id`),
KEY `for_role_id` (`role_id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
用户实体类需要实现
UserDetails
接口。
public class Admin implements UserDetails {
private int id;
private String username;
private String password;
private long created;
private long lastLogin;
private int loginCount;
private boolean enabled;
private boolean locked;
private List<Role> roles;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public long getCreated() {
return created;
}
public void setCreated(long created) {
this.created = created;
}
public long getLastLogin() {
return lastLogin;
}
public void setLastLogin(long lastLogin) {
this.lastLogin = lastLogin;
}
public int getLoginCount() {
return loginCount;
}
public void setLoginCount(int loginCount) {
this.loginCount = loginCount;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isLocked() {
return locked;
}
public void setLocked(boolean locked) {
this.locked = locked;
}
// 当前账号是否未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 当前账号是否未锁定
@Override
public boolean isAccountNonLocked() {
return !locked;
}
// 当前账号密码是否未过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 当前账号是否可用
@Override
public boolean isEnabled() {
return true;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for(Role role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName().toUpperCase()));
}
return authorities;
}
}
接口有7个方法
方法 | 说明 |
| 获取所具有的角色信息 |
| 获取密码 |
| 获取用户名 |
| 账号是否未过期 |
| 账号是否未锁定 |
| 账号密码是否未过期 |
| 账号是否可用 |
其中getAuthorities获取角色信息
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for(Role role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName().toUpperCase()));
}
return authorities;
}
说下这里踩的坑。当我们不使用ROLE_做前缀时,会报
spring security There was an unexpected error (type=Forbidden, status=403).
在网上看到说:
路径权限规则匹配中配置的是:ADMIN 这里程序猿不可以配置ROLE_开头的角色 不然直接报BUG
自定义权限验证中就要配置用户的权限:ROLE_ADMIN 需要加上ROLE_开头
看来下WebSecurityConfigurerAdapter的configure(HttpSecurity http)的hasRole
.antMatchers("/backend/**")
.hasRole("ADMIN")
在
org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer
第119行有说明
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
} else {
return "hasRole('ROLE_" + role + "')";
}
}
如果传入的权限有ROLE_直接抛出异常,否则返回以ROLE_未前缀的权限规则。而我数据库里储存的是小写,所以转为大写,并加上前缀ROLE_
第二个坑是:
Cannot serialize; nested exception is org.springframework.core.serializer.support, ... com.xxx.model.Role
这里需要模型实现接口Serializable
public class Role implements Serializable {
private int id;
private String name;
private String title;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
service实现需要实现UserDetailsService接口
public interface AdminService extends UserDetailsService {
public Admin findByName(String username);
public List<Role> getUserRolesById(int id);
}
接口实现类
public class AdminServiceImpl implements AdminService {
@Autowired
private AdminDao adminDao;
@Override
public Admin findByName(String username) {
return adminDao.findByName(username);
}
@Override
public List<Role> getUserRolesById(int id) {
return adminDao.getUserRolesById(id);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Admin admin = adminDao.findByName(username);
if (admin == null) {
throw new UsernameNotFoundException("admin not defined");
}
List<Role> roles = adminDao.getUserRolesById(admin.getId());
admin.setRoles(roles);
return admin;
}
}
然后就是实现配置,配置需要继承WebSecurityConfigurerAdapter
public class BlogSecurity extends WebSecurityConfigurerAdapter {
@Autowired
AdminService adminService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(adminService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/backend/**")
.hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/auth/login") // 自定义登录页面 默认/login
.loginProcessingUrl("/auth/login-in") // 自定义登录post页面,默认/login,如果自定义了loginPage,则默认为loginPage的页面
.defaultSuccessUrl("/auth/dashboard") // 自定义登录成功页面,
.failureUrl("/auth/login?error=fail") // 自定义登录失败页面
.and()
.logout() // 自定义跳出页面, 这里注意,如果么有禁用csrf, 需要用post跳出
.logoutSuccessUrl("/auth/login?error=logout"); // 自定义跳出成功页面
}
}
这里也踩了一个坑。
如果
auth.userDetailsService(adminService).passwordEncoder(new BCryptPasswordEncoder());
改成
auth.userDetailsService(adminService);
就会报There is no PasswordEncoder mapped for the id "null"
这在网上有说明
Example 20. DelegatingPasswordEncoder Storage Format
{id}encodedPassword
Such that id
is an identifier used to look up which PasswordEncoder
should be used and encodedPassword
is the original encoded password for the selected PasswordEncoder
. The id
must be at the beginning of the password, start with {
and end with }
. If the id
cannot be found, the id
will be null. For example, the following might be a list of passwords encoded using different id
. All of the original passwords are "password".
Example 21. DelegatingPasswordEncoder Encoded Passwords Example
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
The first password would have a PasswordEncoder id of bcrypt and encodedPassword of $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG . When matching it would delegate to BCryptPasswordEncoder | |
The second password would have a PasswordEncoder id of noop and encodedPassword of password . When matching it would delegate to NoOpPasswordEncoder | |
The third password would have a PasswordEncoder id of pbkdf2 and encodedPassword of 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc . When matching it would delegate to Pbkdf2PasswordEncoder | |
The fourth password would have a PasswordEncoder id of scrypt and encodedPassword of $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= When matching it would delegate to SCryptPasswordEncoder | |
The final password would have a PasswordEncoder id of sha256 and encodedPassword of 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 . When matching it would delegate to StandardPasswordEncoder |
Some users might be concerned that the storage format is provided for a potential hacker. This is not a concern because the storage of the password does not rely on the algorithm being a secret. Additionally, most formats are easy for an attacker to figure out without the prefix. For example, BCrypt passwords often start with |
Password Encoding
The idForEncode
passed into the constructor determines which PasswordEncoder
will be used for encoding passwords. In the DelegatingPasswordEncoder
we constructed above, that means that the result of encoding password
would be delegated to BCryptPasswordEncoder
and be prefixed with {bcrypt}
. The end result would look like:
Example 22. DelegatingPasswordEncoder Encode Example
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
Password Matching
Matching is done based upon the {id}
and the mapping of the id
to the PasswordEncoder
provided in the constructor. Our example in Password Storage Format provides a working example of how this is done. By default, the result of invoking matches(CharSequence, String)
with a password and an id
that is not mapped (including a null id) will result in an IllegalArgumentException
. This behavior can be customized using DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)
.
By using the id
we can match on any password encoding, but encode passwords using the most modern password encoding. This is important, because unlike encryption, password hashes are designed so that there is no simple way to recover the plaintext. Since there is no way to recover the plaintext, it makes it difficult to migrate the passwords. While it is simple for users to migrate NoOpPasswordEncoder
, we chose to include it by default to make it simple for the getting started experience.
在
org.springframework.security.crypto.password.DelegatingPasswordEncoder
有说明。
登录页面
<form class="pt-3" name="f" th:action="@{/backend/login-in}" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<div class="form-group">
<input type="text" class="form-control form-control-lg" name="username" id="username" placeholder="用户名">
</div>
<div class="form-group">
<input type="password" class="form-control form-control-lg" name="password" id="password" placeholder="密码">
</div>
<div class="mt-3">
<input type="submit" class="btn btn-block btn-primary btn-lg font-weight-medium auth-form-btn" value="登录" />
</div>
</form>