用户与角色 Springboot + Security + Mybatis
安全认证与授权包括基于内存的认证,基于数据库的认证,高级配置
基于内存的认证是直接将用户名,密码,和用户与角色的url授权信息直接写在配置文件中,不需要读取数据库;
基于数据库的认证则是从数据库中直接读取用户名和密码,用户与角色的url授权信息仍然写在配置文件中;
高级配置中将这三条信息都存在数据库表中,为的是实现动态配置URL权限。
配置Security重点在于WebSecurityConfigurerAdapter
本文代码参考《Spring Boot + Vue 全栈开发实战》王松 第十章
基于内存的认证
- 添加security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 自定义WebSecurityConfigurerAdapter
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(10);//密码加密方式,BCrypt强哈希函数
}
@Override//配置三名用户的的角色和密码,密码明文全是123
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("root").password("$2a$10$yKbvsbp.mwbBmon34q3k/uZdXWbsh/8tDkChKqAbq/3xm7xC2VCTi").roles("ADMIN","DBA")
.and()
.withUser("admin").password("$2a$10$bvW.9j56BG0XPAA8icJQhuU4qUS7zT.lCayiqBry45HqJMajVV2JC").roles("ADMIN","USER")
.and()
.withUser("zhsh").password("$2a$10$UCu/YkcMgyvbumqM8unEeOSpDtCVAmCG0pncWS6CxcYSIGpQCcBtm").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")//配置URL的角色要求
.antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')")
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().authenticated()
.and()
.formLogin().loginProcessingUrl("/login")//开启表单登录
.usernameParameter("name").passwordParameter("passwd")
.successHandler(new AuthenticationSuccessHandler() {//登录成功的一段JSON提示
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp,
Authentication auth) throws IOException {
Object principal = auth.getPrincipal();
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
resp.setStatus(200);
Map<String,Object> map = new HashMap<>();
map.put("status",200);
map.put("msg",principal);
ObjectMapper om = new ObjectMapper();
out.write(om.writeValueAsString(map));
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {//登录失败的一段JSON提示
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp,
AuthenticationException e) throws IOException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
resp.setStatus(401);
Map<String,Object> map = new HashMap<>();
map.put("status",401);
if(e instanceof LockedException){
map.put("msg","账号被锁定,登录失败!");
}else if(e instanceof BadCredentialsException){
map.put("msg","账户名或密码输入错误,登陆失败!");
}else if(e instanceof DisabledException){
map.put("msg","账号被禁用,登陆失败!");
}else if(e instanceof AccountExpiredException){
map.put("msg","账户已过期,登陆失败!");
}else if(e instanceof CredentialsExpiredException){
map.put("msg","密码已过期,登陆失败!");
}else{
map.put("msg","登录失败!");
}
ObjectMapper om = new ObjectMapper();
out.write(om.writeValueAsString(map));
out.flush();
out.close();
}
})
.permitAll()
.and()
.logout().logoutUrl("/logout")//开启注销登录
.clearAuthentication(true).invalidateHttpSession(true)
.addLogoutHandler(new LogoutHandler() {
@Override
public void logout(HttpServletRequest req, HttpServletResponse resp, Authentication auth) { }
})
.logoutSuccessHandler(new LogoutSuccessHandler() {//注销成功后的业务逻辑
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp,
Authentication auth) throws IOException {
resp.sendRedirect("/loginPage");
}
})
.and()
.csrf().disable();//关闭csrf,跨站请求伪造
}
}
- 编写controller进行授权测试
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "Hello!";
}
@GetMapping("/admin/hello")
public String admin(){
return "hello admin";
}
@GetMapping("/user/hello")
public String user(){
return "hello user";
}
@GetMapping("/db/hello")
public String dba(){
return "hello dba";
}
@GetMapping("/loginPage")
public ModelAndView loginPage(){
ModelAndView mv = new ModelAndView("index");
return mv;
}
}
基于数据库的认证
- 设计数据表
user,role,user_role三张表 - 创建实体类
//因为UserDetails自带Boolean类型的isEnabled函数,会与@Data中的getEnabled有冲突,所以不能使用@Data
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean locked;
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role: roles){
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {return password;}
@Override
public String getUsername() { return username; }
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return !locked; }
@Override
public boolean isCredentialsNonExpired() {return true; }
@Override
public boolean isEnabled() { return enabled; }
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public void setUsername(String username) { this.username = username; }
public void setPassword(String password) { this.password = password; }
public Boolean getLocked() { return locked; }
public void setLocked(Boolean locked) {this.locked = locked; }
public List<Role> getRoles() { return roles; }
public void setRoles(List<Role> roles) { this.roles = roles; }
}
@Data
public class Role {
private Integer id;
private String name;
private String nameZh;
}
- 添加依赖与配置
<!--添加MyBatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!--添加mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--添加数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
######## 数据库配置 ########
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123
spring.datasource.tomcat.max-idle=10
spring.datasource.tomcat.max-active=50
spring.datasource.tomcat.max-wait=10000
spring.datasource.tomcat.initial-size=5
# 采用隔离级别为读写提交
spring.datasource.tomcat.default-transaction-isolation=2
######### MyBatis配置 ########
# 映射文件
mybatis.mapper-locations=classpath:com/mhr/mhr/security/mapper/*.xml
- 创建dao与mapper
//dao
@Mapper
public interface UserMapper {
User loadUserByUsername(String username);
List<Role> getUserRolesByUid(Integer id);
}
//mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mhr.mhr.security.dao.UserMapper">
<select id="loadUserByUsername" resultType="com.mhr.mhr.security.pojo.User">
select * from user where username=#{username}
</select>
<select id="getUserRolesByUid" resultType="com.mhr.mhr.security.pojo.Role">
select * from role r,user_role ur where r.id=ur.rid and ur.uid=#{id}
</select>
</mapper>
- 创建service与controller
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(s);
if (user==null){
throw new UsernameNotFoundException("账户不存在!");
}
user.setRoles(userMapper.getUserRolesByUid(user.getId()));
return user;
}
}
@RestController
public class UserController {
@Autowired
UserService userService;
@GetMapping("/loadUserByUsername")
public User loadUserByUsername(String username){
return (User) userService.loadUserByUsername(username);
}
}
- 配置Spring Security
@Configuration
public class WebSecurityConfig2 extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override //将UserService配置到AuthenticationManagerBuilder中,没有配置内存用户
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("dba")
.antMatchers("/db/**").hasRole("user")
.anyRequest().authenticated()
.and()
.formLogin().loginProcessingUrl("/login").permitAll()
.and()
.csrf().disable();
}
}
高级配置
动态配置url权限
- 添加数据表
menu,menu_role - 编写dao和mapper
//dao
@Mapper
public interface MenuMapper {
List<Menu> getAllMenus();
}
//mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mhr.mhr.security.dao.MenuMapper">
<resultMap id="BaseResultMap" type="com.mhr.mhr.security.pojo.Menu">
<id property="id" column="id"/>
<result property="pattern" column="pattern"/>
<collection property="roles" ofType="com.mhr.mhr.security.pojo.Role">
<id property="id" column="id"/>
<result property="name" column="rname"/>
<result property="nameZh" column="rnameZh"/>
</collection>
</resultMap>
<select id="getAllMenus" resultMap="BaseResultMap">
select m.*,r.id as rid,r.name as rname,r.nameZh as rnameZh
from menu m
left join menu_role mr on m.`id`=mr.`mid`
left join role r on mr.`rid`=r.`id`
</select>
</mapper>
- 自定义FilterInvocationSecurityMetadataSource
//主要实现接口中的getAttributes方法
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuMapper menuMapper;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation)o).getRequestUrl();
List<Menu> allMenus = menuMapper.getAllMenus();
for (Menu menu: allMenus){
if(antPathMatcher.match(menu.getPattern(),requestUrl)){ //ant风格的url匹配
List<Role> roles = menu.getRoles();
String[] roleArr = new String[roles.size()];
for (int i = 0; i < roleArr.length; i++){
roleArr[i] = roles.get(i).getName();
}
return SecurityConfig.createList(roleArr);
}
}
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() { return null; }
@Override
public boolean supports(Class<?> aClass) { return FilterInvocation.class.isAssignableFrom(aClass); }
}
- 自定义AccessDecisionManager
// 进行角色信息的比对
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication auth, Object o, Collection<ConfigAttribute> ca){
Collection<? extends GrantedAuthority> auths = auth.getAuthorities();
for (ConfigAttribute configAttribute : ca){
if("ROLE_LOGIN".equals(configAttribute.getAttribute())
&& auth instanceof UsernamePasswordAuthenticationToken){
return;
}
for (GrantedAuthority authority: auths){
if (configAttribute.getAttribute().equals(authority.getAuthority())){
return;
}
}
}
throw new AccessDeniedException("权限不足!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) { return true; }
@Override
public boolean supports(Class<?> aClass) {return true; }
}
- 配置Spring Security
@Configuration
public class WebSecurityConfig3 extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(cfisms());
o.setAccessDecisionManager(cadm());
return o;
}
})
.and()
.formLogin().loginProcessingUrl("/login").permitAll()
.and()
.csrf().disable();
}
@Bean
CustomFilterInvocationSecurityMetadataSource cfisms(){ return new CustomFilterInvocationSecurityMetadataSource(); }
@Bean
CustomAccessDecisionManager cadm(){ return new CustomAccessDecisionManager(); }
}