SpringBoot整合SpringSecurity
pom文件
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
实体类准备
@Entity
@Table(name = "admin")
@Data
//需要实现UserDetails接口
public class Admin implements UserDetails {
@Id
@GeneratedValue
@Column(name="admin_id")
private Integer adminId;
@Column(name = "admin_name")
private String adminName;
@Column(name = "admin_pwd")
private String adminPwd;
@Column(name = "admin_gender")
private String adminGender;
@Column(name = "admin_img")
private String adminImg;
@Column(name = "admin_authority")
private Integer adminAuthority;
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//给予角色权限,这里AuthorityPoint类是实现了GrantedAuthority的,模拟Admin的权限信息,实际中可从权限表中查出
AuthorityPoint authorityPoint = new AuthorityPoint();
authorityPoint.setAuthorityId(adminAuthority+"");
return Arrays.asList(authorityPoint);
}
@Override
public String getPassword() {
return adminPwd;
}
@Override
public String getUsername() {
return adminName;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
模拟的权限类
@Data
public class AuthorityPoint implements GrantedAuthority {
private String authorityId;
public AuthorityPoint() {
}
//表示该类的authorityId属性作为权限名
@Override
public String getAuthority() {
return authorityId;
}
}
中间层
@Service
//需要继承UserDetailsService
public interface AdminService extends UserDetailsService {
}
@Service
public class AdminServiceImpl implements AdminService {
private Logger log = LoggerFactory.getLogger(AdminServiceImpl.class);
//使用Jpa做持久层
@Autowired
private AdminReposity adminReposity;
//返回查询出的实体信息
@Override
public UserDetails loadUserByUsername(String name) throws
UsernameNotFoundException {
log.warn("===============传入的name:{}=============",name);
Admin admin = adminReposity.getByAdminName(name);
log.warn("查询出的admin:{}",admin.getAdminName());
return admin;
}
}
@Repository
public interface AdminReposity extends JpaRepository<Admin,Integer>{
//这里从Admin表查出信息,实际上也要查出对应的权限信息
@Query("select a from Admin a where a.adminName = ?1")
Admin getByAdminName(String name);
}
主要配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AdminService adminService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private DataSource dataSource; // 数据源
//认证失败的处理器
@Bean
public MyFalureHandler myFalureHandler(){
return new MyFalureHandler();
}
//退出的处理器
@Bean
public MyLogoutHandler myLogoutHandler(){
return new MyLogoutHandler();
}
//注入加密对象
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(adminService).passwordEncoder(passwordEncoder);
}
//用于设置rememberMe往数据库存储token
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource); // 设置数据源
return tokenRepository;
}
/**
* 开启部分路径配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().
antMatchers("/css/**","/font/**","/img/**","/js/**","/json/**",
"/layer/**").permitAll()
.anyRequest().authenticated() //访问其他需要认证
.and()
.formLogin()
.loginPage("/admin") //设置取代默认登录页
.loginProcessingUrl("/admin/login") //当在登录页跳转该url时就触发Security的登录请求
.defaultSuccessUrl("/admin/index") //设置登录成功页面,不要使用successForwardUrl("/admin/index"),可能会出现405错误,原因不知,巨坑!
// .failureForwardUrl("/admin")
// .failureHandler(myFalureHandler())
.permitAll()
.and()
.logout().logoutUrl("/admin/logout").logoutSuccessUrl("/admin")//设置触发注销的url以及注销后跳转的url
.invalidateHttpSession(true)
.permitAll()
.and()
.rememberMe().tokenValiditySeconds(60*6).tokenRepository//设置rememberMe的保存时长
(persistentTokenRepository()) //把rememberMe信息存到数据库
.and()
.csrf() //禁用csrf
.disable();
}
}
自己设置的登录页面
<body id="login">
<div class="login">
<h2>管理员登录</h2> <!-- 注意action属性设为配置文件中的触发登录url -->
<form class="layui-form" method="post" action="/admin/login">
<p style="color: red" th:if="${param.error}">名字或密码有误~</p>
<br>
<div class="layui-form-item"> <!-- 注意name属性设为username -->
<input type="text" name="username" lay-verify="required"
lay-reqText="管理员名不能为空"
placeholder="管理员名" class="layui-input" >
<i class="layui-icon input-icon"></i>
</div>
<div class="layui-form-item"> <!-- 注意name属性设为password -->
<input type="password" name="password" lay-verify="required"
lay-reqText="密码不能为空"
placeholder="密码" class="layui-input">
<i class="layui-icon input-icon"></i>
</div>
<div class="layui-form-item"> <!-- 注意name属性设为remember-me -->
<input type="checkbox" name="remember-me" lay-skin="primary" title="记住密码" checked=""> <a class="back" href="javascript:;" style="margin-top: 10px">忘记密码</a>
</div>
<div class="layui-form-item">
<button style="width: 100%" class="layui-btn" lay-submit lay-filter="login">立即登录</button>
</div>
</form>
</div>
这个页面代码直接复制黏贴肯定是不行的,其实自己简单写一个表单,注意把username和password属性,remember-me等属性写对,action也要和配置文件中的**.loginProcessingUrl("/admin/login")**一样,注意:这个url不需要我们在Controller控制器中准备该url方法,这是Security使用的,包括logout路径也不需要我们自己准备
RememberMe功能准备
在数据库里建一张表
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
这样配合上面的配置文件就可以将token保存在数据库了
application.yml文件中没什么好说的,配置了jpa和druid的一些基本信息,不是重点
测试
当你试图访问任何地址时,会根据配置文件中的 loginPage("/admin") 自动跳转到这个url,因为我们没有任何认证,在配置文件中也没有赋权,当然这个url会访问到我们的Controller控制器的方法
@Controller
@RequestMapping("/admin")
public class AdminController {
private Logger log = LoggerFactory.getLogger(this.getClass());
@GetMapping({"","/"})
public String admin(HttpSession session){
return "login";
}
}
就是返回一个login.html页面,当我们默认的登录页,如图:
当你点击登录后,跳转loginProcessingUrl("/admin/login") 这个路径,触发Security的登录,这个路径不是我们controller层的东西
defaultSuccessUrl("/admin/index") 登录成功跳转页面
failureHandler(myFalureHandler()) 登录失败跳转的处理器,myFalureHandler()这个方法在配置文件中,就是返回一个我们自定义的处理器MyFalureHandler ,它的代码如下
public class MyFalureHandler implements AuthenticationFailureHandler {
/**
* 认证失败的处理
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse
response, AuthenticationException e) throws IOException,
ServletException {
System.out.println("============认证失败页面============");
response.sendRedirect("/admin");
}
}
在这个方法中做你的失败处理逻辑,比如跳转到登录页面,有一个问题,我在做跳转到登录页面时,在这个方法中存储了一些提示信息,往Session域,Request域,RedirectAttributes中都尝试过存储数据然后在登录页面提示失败信息,但是没有用,这是为什么?有人知道的话,欢迎告诉我这个菜鸡
注意从数据库取出的用户密码是加密后的数据,因为我们认证时使用了加密
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(adminService).passwordEncoder(passwordEncoder);
}
你可以在测试代码中使用BCryptPasswordEncoder去加密你的用户密码,再存储到数据库模拟
权限相关
我们在实体类中模拟取出Admin的相关权限,权限可以是多个,这里只是模拟并未从数据库中取
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//给予角色权限,这里AuthorityPoint类是实现了GrantedAuthority的,模拟Admin的权限信息,实际中可从权限表中查出
AuthorityPoint authorityPoint = new AuthorityPoint();
authorityPoint.setAuthorityId(adminAuthority+"");
return Arrays.asList(authorityPoint);
}
开启注解
@EnableGlobalMethodSecurity(securedEnabled = true)
在相关的Controller中增加权限控制
@Secured("1") //需要权限名为1的用户才能访问,否则403
@GetMapping("/userlist")
public String userchart(){
return "admin/userlist";
}
这样就能控制权限了