spring之spring security

首先加入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个方法

方法说明
Collection<? extends GrantedAuthority> getAuthorities
获取所具有的角色信息
String getPassword
获取密码
String getUsername
获取用户名
boolean isAccountNonExpired
账号是否未过期
boolean isAccountNonLocked
账号是否未锁定
boolean isCredentialsNonExpired
账号密码是否未过期
boolean isEnabled
账号是否可用

 

其中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 $2a$.

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>

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值