Spring Security的使用(一)
整体流程
一个使用Spring Security编写的注册登录的Demo,分管理员和用户两种角色,登陆后可通过Spring Security控制页面的控件显隐达到区分管理员和用户的目的。
代码部分
Spring Security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--springsecurity4 要指定3.0以上版本,否则权限标签可能无法工作-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
Spring Security配置类
配置授权方式
重写protected void configure(HttpSecurity http)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.formLogin()
//默认登录页面
.loginPage("/login.html")
//和登录时的表单的action地址对应,表明该form请求由spring security处理
.loginProcessingUrl("/authentication/form")
//默认登录请求成功页面
.defaultSuccessUrl("/index",true)
//认证失败跳转页面
.failureForwardUrl("/errorinfo")
.and()
.authorizeRequests()
//放行一下请求
.antMatchers("/login.html","/druid/**","/static/**","/register").permitAll()
.anyRequest()//对任何请求
.authenticated()//开启认证
.and()
//关闭druid可视化页面的防劫持(如果使用druid内置的可视化监控平台)
.headers().frameOptions().disable()
.and()
//开放Spring Security的“/logout”注销
.logout().permitAll();//开放注销访问
}
配置登录认证
重写protected void configure(AuthenticationManagerBuilder auth)
//登录认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(LoginUserCheckUtil());
}
将Spring Security默认的加密方式进行注入
//spring security提供的BCrypt加密方式
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
将具体执行登录认证的过程注入到配置类中
//将登录认证工具类注入到Bean
@Bean
UserDetailsService LoginUserCheckUtil(){
return new LoginUserCheckUtil();
}
进行登录认证的工具类
利用@Component将本工具类变为一个Spring的组件,因为项目持久层使用的是mybatis,所以将Mapper注入到工具类中方便执行sql对数据库进行操作
@Component
public class LoginUserCheckUtil implements UserDetailsService {
@Autowired
private LoginMapper loginMapper;
//参数username就是在页面登录时输入的用户名,会自动传到此方法中
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//执行sql以传入的用户名为条件在数据库进行查询
T_User t_user = loginMapper.selectuserByUsername(username);
//声明List存储该角色的权限集合
ArrayList<GrantedAuthority> grantedAuthority = new ArrayList<>();
//利用用户名获得其角色再查询数据库获得该角色的权限
String authorities = loginMapper.selectRoleByUserRoleid(t_user.getRole_id());
//对角色进行判断,并赋予其对应的权限
//比如它有个表达式hasRole("ADMIN")。那它实际上查询的是用户权限集合中是否存在字符串"ROLE_ADMIN"。如果你从角色表中取出用户所拥有的角色时不加上"ROLE_"前缀,那验证的时候就匹配不上了。
if(authorities.equals("admin")){
authorities="ROLE_ADMIN";
}else if (authorities.equals("user")){
authorities="ROLE_USER";
}
//用获得的权限进行初始化
GrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authorities);
//添加到权限集合中
grantedAuthority.add(simpleGrantedAuthority);
return new User(t_user.getUsername(),t_user.getPassword(),true,true,true,true,grantedAuthority);
}
}
返回一个User对象,这个User时Spring Security内置的对象,主要使用用户名,密码,权限三个属性。
也可以自定义对象需要实现UserDetails。
至此登录认证过程结束,认证成功会返回一个带有用户名,密码,权限的User对象;否则认证失败,会跳转到授权时 .failureForwardUrl指向的URL。
在注册时别忘记也是用BCryptPasswordEncoder方法进行加密,否则对比不成功(当然也可使用自定义的加密算法)。
页面中对控件的权限控制
sec:authorize=“hasRole(‘ROLE_USER’)”
sec:authorize=“hasRole(‘ROLE_ADMIN’)”
利用上面的属性进行判断然后控制页面控件的显隐,对不同权限的用户开放不同的页面功能
<li id="yuangong" class="layui-nav-item" sec:authorize="hasRole('ROLE_USER')">
<a href="javascript:;">用户功能
<span class="layui-nav-more"></span>
</a>
<dl class="layui-nav-child">
<dd><a href="Leavepage" target="info"><span>我要请假</span></a></dd>
<dd><a href="worktext" target="info"><span>工作日志</span></a></dd>
<dd><a href="History" target="info"><span>历史公告</span></a></dd>
</dl>
</li>
<li id="zhineng" class="layui-nav-item" sec:authorize="hasRole('ROLE_ADMIN')">
<a href="javascript:;">管理员功能
<span class="layui-nav-more"></span>
</a>
<dl class="layui-nav-child">
<dd><a href="managerinfo" target="info"><span>员工档案</span></a></dd>
<dd><a href="Leavemanage" target="info"><span>请假管理</span></a></dd>
<dd><a href="worktextmanager" target="info"><span>日志管理</span></a></dd>
<dd><a href="announcement" target="info"><span>公告发布</span></a></dd>
<dd><a href="History" target="info"><span>历史公告</span></a></dd>
</dl>
</li>
BCryptPasswordEncoder加密算法
BCryptPasswordEncoder算法内部使用的加随机盐(salt)的方式保证安全性,以往的MD5加密方式对相同字符串加密出的密文相同,存在一定的风险;但现在通过随机盐的方式保证了密文的安全性。
加随机盐
首先得到算法的版本、强度和随机安全实例
public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) {
if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
throw new IllegalArgumentException("Bad strength");
}
this.version = version;
this.strength = strength == -1 ? 10 : strength;
this.random = random;
}
生成随机盐
参数:真实密码
public String encode(CharSequence rawPassword) {
String salt;
if (random != null) {
salt = BCrypt.gensalt(version.getVersion(), strength, random);
} else {
salt = BCrypt.gensalt(version.getVersion(), strength);
}
return BCrypt.hashpw(rawPassword.toString(), salt);
}
matches方法用来校验登录密码和数据库存储的密码是否一致
参数:真实密码,密码的密文
首先校验密码是否存在
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
logger.warn("Empty encoded password");
return false;
}
if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
logger.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
将纯文本密码和hash后的密码传入
public static boolean checkpw(String plaintext, String hashed) {
return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
}
利用StandardCharsets编码常量,将字符串密码转换为字符数组进行比较
static boolean equalsNoEarlyReturn(String a, String b) {
return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
}
关于权限中的defaultsuccessurl和forsuccessurl的区别参考:
https://my.oschina.net/lenglingx/blog/4311577