说说SpringSecurity的入门级别的使用吧
在本人大四即将毕业的时候,一个学弟找我做他的课设,这个时候再去做他们的课设,真的觉得太简单了,SpringBoot帮忙做的事太多了,又是简单的CRUD,前端用的模板引擎,为了让他顺利上岸,我觉得帮他加上权限,并尝试了我没有用过的
JPA
1、说说安全
安全就得有,在SpringBoot实战一书中,作者已经说明,门要上锁头,信息可能事我们最有价值的东西了。老哥你还用拦截器?还用Session保存用户信息?别了啊!
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1.1、配置类
引入依赖之后,你的应用的大门就已经上锁了,不信,你启动项目,绝对会跳出一个登录表单!
package com.cbf.security;
import cn.hutool.core.util.ArrayUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
import java.util.List;
/**
* @author wangnaixing
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
List<String> urls = ignoreUrlsConfig.getUrls();
String[] urlArray = ArrayUtil.toArray(urls, String.class);
http.authorizeRequests()
.antMatchers("/").access("permitAll()")
.antMatchers("/courses").access("hasAnyRole('ROLE_USER')")
.antMatchers(urlArray).access("permitAll()")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/")
.loginProcessingUrl("/authenticate")
.usernameParameter("user")
.passwordParameter("pwd")
.defaultSuccessUrl("/permission/getPermissionList",true)
.and()
.headers().frameOptions().disable()
.and()
.logout()
.logoutSuccessUrl("/toLogin");
}
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(encoder());
}
}
1.2、让SpringSecurity承认的用户细节
实现了
UserDetails
接口的类,就等于告诉了SpringSecurity,这是就是我当前用户的信息,就好像你在Session中存入当前用户信息一样。想用的时候,我就可以取出来!
package com.cbf.security;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
/**
* @author by wangnaixing
* @Description 用户详情
* @Date 2021/12/25 16:04
*/
public class MyUserDetail implements UserDetails {
/**
* 用户名
*/
private final String username;
/**
* 密码
*/
private final String password;
/**
* 用户的权限集合
*/
Collection<? extends GrantedAuthority> grantedAuthorities;
public MyUserDetail(String username, String password, List<GrantedAuthority> grantedAuthorities) {
this.username = username;
this.password =password;
this.grantedAuthorities=grantedAuthorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.grantedAuthorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
1.3、自定义处理UserDetail
的业务处理类
和数据库打通,从数据库中根据用户名获取用户信息,可以说用户名在SpringSecurity中很关键,通过这个用户就能得到用户的一切,而做这个事情的人,就是
MyUserDetailsService
,通过实现UserDetailsService
,具备了loadUserByUsername(String username)
的能力。
package com.cbf.security;
import com.cbf.pojo.Permission;
import com.cbf.pojo.Role;
import com.cbf.pojo.User;
import com.cbf.repo.UserDao;
import lombok.extern.slf4j.Slf4j;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @author by wangnaixing
* @Description
* @Date 2021/12/25 14:35
*/
@Slf4j
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserDao userRepo;
/**
* 根据用户名返回用户详情
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("=====系统正在进行身份认证中====");
User user = userRepo.findByUsername(username);
if (user != null) {
//把数据库中的用户包装成为用户详情
System.out.println("认证成功!当前用户->"+username);
//拿到用户权限:角色+资源
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
List<Role> roleList = user.getRoleList();
List<Permission> permissionList = new ArrayList<>();
roleList.forEach(x-> permissionList.addAll(x.getPermissionList()));
roleList.stream()
.filter(x->x.getRoleCode()!=null)
.forEach(x->grantedAuthorities.add(new SimpleGrantedAuthority(x.getRoleCode())));
permissionList.stream()
.filter(x->x.getPermission()!=null)
.forEach(x->grantedAuthorities.add(new SimpleGrantedAuthority(x.getPermission())));
log.info("--用户权限---");
grantedAuthorities.forEach(System.out::println);
return new MyUserDetail(username, user.getPwd(),grantedAuthorities);
}
throw new UsernameNotFoundException(
"User '" + username + "' not found");
}
}
1.4、基于RCBA
,根据用户名获取用户权限
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z8EtjMFY-1640487041766)(D:\my_notebook\typora笔记图片统一管理处\image-20211226103521237.png)]
2、让不同的用户登录有不同的菜单
2.1、后端提供接口
根据用户名,连表,拿到权限表的信息,权限表,就那几个字段,名称啊,url啦,名称就是菜单名了,url就是点击以后怎么跳的问题了,存在级联关联,ID,PID那种,本人用流处理了一下
package com.cbf.web;
import com.cbf.dto.PermissionNode;
import com.cbf.pojo.Permission;
import com.cbf.pojo.User;
import com.cbf.repo.UserDao;
import com.cbf.security.MyUserDetail;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 权限资源控制器
* @author by wangnaixing
* @Description
* @Date 2021/12/25 13:03
*/
@Controller
@RequestMapping("/permission")
public class PermissionController {
@Autowired
private UserDao userDao;
@RequestMapping("/getPermissionList")
public String getPermissionList(Model model, @AuthenticationPrincipal MyUserDetail principal){
List<Permission> permissionList = new ArrayList<>();
//调用服务,根据用户名查询用户信息
User user = userDao.findByUsername(principal.getUsername());
//获取该用户权限信息
user.getRoleList().forEach(x-> permissionList.addAll(x.getPermissionList()));
//将权限信息转为树形
List<PermissionNode> permissions = permissionList.stream()
.filter(x -> x.getPId().equals(0L))
.map(x -> buildTree(x, permissionList))
.collect(Collectors.toList());
model.addAttribute("permissions",permissions);
return "index";
}
/**
* 构建树形结构
* @param permission
* @param permissionList
* @return
*/
private PermissionNode buildTree(Permission permission, List<Permission> permissionList) {
PermissionNode node = new PermissionNode();
BeanUtils.copyProperties(permission,node);
List<PermissionNode> children =
permissionList.stream()
.filter(x -> x.getPId().equals(permission.getId()))
.map(x -> buildTree(x, permissionList))
.collect(Collectors.toList());
node.setChildren(children);
return node;
}
}
package com.cbf.dto;
import com.cbf.pojo.Permission;
import java.util.ArrayList;
import java.util.List;
/**
* 权限的树形节点
* @author by wangnaixing
* @Description
* @Date 2021/12/25 13:55
*/
public class PermissionNode extends Permission {
public List<PermissionNode> getChildren() {
return children;
}
public void setChildren(List<PermissionNode> children) {
this.children = children;
}
private List<PermissionNode> children = new ArrayList<PermissionNode>();
}
2.2、侧边栏动态变化
<div class="leftnav">
<div class="leftnav-title">
<strong><span class="icon-list"></span>菜单列表</strong>
</div>
<div th:each="item:${permissions}">
<h2>
<span class="icon-user" th:text="${item.name}"></span>
</h2>
<ul style="display: block" th:if="${item.children!=null}">
<li th:each="item2:${item.children}"><a th:href="${item2.url}" target="right"><span class="icon-caret-right" th:text="${item2.name}"></span></a></li>
</ul>
</div>
</div>
3、拦截吧,没登陆不给进来,没权限不给访问接口。
3.1、登录表单
处理的路径,处理的用户名,密码统统自定义。
<form th:action="@{/authenticate}" method="POST">
<div class="panel loginbox">
<div class="text-center margin-big padding-big-top">
<h1>学生体温填报管理信息系统V1.0</h1>
</div>
<div class="panel-body"
style="padding: 30px; padding-bottom: 10px; padding-top: 10px;">
<div class="form-group">
<div class="field field-icon-right">
<input type="text" class="input input-big" name="user"
placeholder="登录账号" data-validate="required:请填写账号" /> <span
class="icon icon-user margin-small"></span>
</div>
</div>
<div class="form-group">
<div class="field field-icon-right">
<input type="password" class="input input-big" name="pwd"
placeholder="登录密码" data-validate="required:请填写密码" /> <span
class="icon icon-key margin-small"></span>
</div>
</div>
<div class="form-group">
<div class="field">
<input type="text" class="input input-big" name="code"
placeholder="填写右侧的验证码" data-validate="required:请填写右侧的验证码" value="6892"/>
<img src="images/passcode.jpg" alt="" width="100"
height="32" class="passcode"
style="height: 43px; cursor: pointer;"
onclick="this.src=this.src+'?'">
</div>
</div>
</div>
<div style="padding: 30px;">
<input type="submit"
class="button button-block bg-main text-big input-big"
value="登录">
</div>
</div>
</form>
3.2、登出表单
<form th:action="@{/logout}" method="POST">
<button type="submit"
class="button button-little bg-red" href="login.html"><span
class="icon-power-off"></span> 退出登录</button>
</from>
3.3、密文的密码
你在配置中注入加密器!数据存的密码一定是密文。
package com.cbf.security;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author by wangnaixing
* @Description
* @Date 2021/12/25 14:43
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class PasswordEncoderTest {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Test
public void test01(){
String password = "123456";
String encode = passwordEncoder.encode(password);
System.out.println(encode);
}
}
3.4、白名单
比如一些静态资源不好拦截吧,再拦截样式都没得了,在配置类配置,他接受的参数是一个可变String类型,为了不把数据写死,我使用了写在yaml的方式,去绑定
3.4.1、application.yaml
secure:
ignored:
urls:
- /swagger-resources/**
- /v2/api-docs/**
- /swagger-ui.html
- /*.html
- /**/*.html
- /**/*.css
- /**/*.js
- /**/*.png
- /**/*.jpg
3.4.2、配置类
怎么注入详情见配置类
package com.cbf.security;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 用于配置白名单资源路径
*
*/
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {
private List<String> urls = new ArrayList<>();
}